BinarySearch для всех вхождений?

Как я могу искать все вхождения значения в массиве, используя BinarySearch? По умолчанию TArray.BinarySearch в System.Generics.Collections возвращает только один индекс.

Пример массива:

A = [1, 2, 3, 3, 3, 6, 7, 8, 9];


person user3764855    schedule 05.07.2014    source источник


Ответы (2)


Бинарный поиск предполагает, что вы уже отсортировали свой массив, поэтому любые другие совпадающие элементы будут сгруппированы вокруг совпадающего элемента, возвращаемого BinarySearch. Справка Delphi XE5 отмечает, что

Если в массиве есть более одного элемента, равного Item, индекс первого совпадения возвращается в FoundIndex. Это индекс любого из совпадающих элементов, не обязательно первого элемента».

Это говорит о том, что вам нужно будет выполнить поиск как вперед, так и назад в массиве, чтобы получить все совпадающие элементы.

person PeterK    schedule 05.07.2014
comment
Я ищу алгоритм, который вернет массив индексов найденных совпадений. - person user3764855; 05.07.2014
comment
Извините, я не понимаю - мой ответ включал своего рода алгоритм, но я действительно не вдавался в подробности. Идея состоит в том, что вы запускаете BinarySearch() и, если возвращается индекс, вы запускаете линейный поиск вперед (i++), начиная с этого индекса, пока не найдете элемент, который не является искомым значением, или вы попали в конец массива. Вы также делаете то же самое в обратном порядке (i--). Вы также добавляете в результирующий массив индексы всех встречающихся вам элементов, содержащих искомое значение. Это помогает? - person PeterK; 05.07.2014
comment
Два бинарных поиска быстрее, чем бинарный поиск + последовательный. - person user3764855; 05.07.2014
comment
@ user3764855: Вам не нужен массив индексов. Поскольку все значения с одинаковым значением будут последовательными, вам нужно только найти начальный и конечный индексы диапазона, в котором они находятся. Вы начинаете с бинарного поиска, чтобы найти одно из них, а затем выполняете последовательный поиск назад и вперед, пока не найдете другие значения. Это НЕ может быть сделано с помощью бинарного поиска, так как все значения одинаковы. - person Rudy Velthuis; 05.07.2014
comment
@RudyVelthuis О чем ты говоришь? pastebin.com/mTzrZf15 Почему это работает для меня? - person user3764855; 05.07.2014
comment
@ user3764855: Используя ваш пример, скажем, мы ищем значение 3, и оно возвращается с индексом 3 (при условии, что индекс основан на 0). Все, что вам нужно сделать, это последовательно выполнять поиск вперед и назад по этому индексу. Я не понимаю, как бинарный поиск, например. часть набора результатов будет быстрее. - person PeterK; 05.07.2014
comment
Трудно сказать, почему это работает для вас. Ваш фрагмент не является SSCCE, поэтому я не могу его проверить. Некоторые из используемых функций и переменных не объясняются. Вероятно, это также зависит от ваших данных, например. - person Rudy Velthuis; 05.07.2014
comment
@RudyVelthuis Это было перенесено из алгоритма C++. Так что ДА, вы МОЖЕТЕ использовать BinarySearch для последнего индекса. - person user3764855; 05.07.2014
comment
@PeterK O (n) против O (Log (n)) - person user3764855; 05.07.2014
comment
Да, вы можете, но, вероятно, в любом случае это просто ухудшится до линейного поиска, просто с большими накладными расходами. Это должно быть проверено с гораздо большим набором чисел. Но вам нужны первый и последний индексы, верно? - person Rudy Velthuis; 05.07.2014
comment
@ user3764855: Как я уже сказал, это, вероятно, потребует больше накладных расходов, чем линейный поиск. Тогда ваш O (log (n)) вам не очень поможет. Линейный поиск, вероятно, быстрее. Но чтобы быть уверенным, вам придется профилировать его с большим разнообразием входных данных, а не теоретизировать. - person Rudy Velthuis; 05.07.2014
comment
@RudyVelthuis Для 100000 записей? Последовательный вам не поможет. - person user3764855; 05.07.2014
comment
@user3764855: user3764855: Ну, если у вас нет массива с идентичными элементами. В любом случае, нет никаких причин, по которым вы не могли бы использовать бинарный поиск по обеим сторонам индексов и оставить O(Log(n)), но вы должны искать элемент всего на 1 единицу меньше или всего на 1 единицу больше, чем значение искать. Даже если значение отсутствует, алгоритм, похожий на бинарный поиск, найдет для вас границу. - person PeterK; 05.07.2014
comment
@PeterK Нет, вы найдете начальный и последний индексы с помощью бинарного поиска. - person user3764855; 05.07.2014
comment
@ user3764855: Позвольте мне уточнить мой ответ. Двоичный поиск может фактически найти значение, ближайшее или равное тому, которое вы ищете. Обычно мы либо возвращаем индекс, либо не найдены. После всех нарезок, выполненных бинарным поиском, единственное значение, которое у вас есть, может быть равным или нет, но, безусловно, это будет значение, ближайшее к искомому значению. Используя это свойство алгоритма, вы можете искать обе части вашего индекса и использовать бинарный поиск, чтобы найти ближайшее значение, которое не равно искомому значению. Индекс этого элемента будет обозначать границу вашего диапазона. - person PeterK; 05.07.2014
comment
@ user3764855: Нет, вы не можете зависеть от двух бинарных поисков, чтобы вернуть первый и последний индексы. Если данные не меняются, а центр зависит от размера, поиск каждый раз будет начинаться с одного и того же места и каждый раз находить один и тот же индекс. Вы должны начать с индекса и проверять предыдущие и последующие элементы, чтобы найти первое и последнее значения индекса, несмотря на ваше мнение, что это не так. (А зачем вы тут спросили, если просто намерены поспорить об ответах?) - person Ken White; 06.07.2014
comment
@user совершенно прав, что бинарный поиск можно использовать для поиска первого или последнего совпадения. - person David Heffernan; 06.07.2014
comment
@DavidHeffernan Это, похоже, работает нормально. Можно ли это сделать по-другому? Или этого достаточно. - person user3764855; 06.07.2014

Позвольте мне объяснить вам проблему немного подробнее. Разница между последовательным поиском и бинарным поиском после того, как вы нашли индекс, зависит от типа данных, которые вы ожидаете найти. 10000 элементов не имеют значения, сколько разных значений искомого элемента. Например, если бы у меня был список из 10000 элементов, состоящий только из 1,2,3,4 и 5. Мы говорим о ситуации, когда каждого значения могут быть тысячи и предпочтительнее будет серия последовательных бинарных переборов. Если бы значения могли варьироваться от 1 до 1000000, у нас было бы гораздо меньше шансов иметь дубликаты, а бинарный поиск с последующим последовательным поиском в обоих направлениях — лучший подход.

Для двоичного, а затем последовательного подхода алгоритм поиска начального и конечного индекса будет следующим:

  1. Найдите индекс с помощью бинарного поиска.
  2. Поиск слева, чтобы найти первый индекс, используя последовательный поиск.
  3. Поиск справа, чтобы найти последний индекс, используя последовательный поиск.

Если вы хотите использовать бинарный поиск, вам нужно будет изменить свой подход и выполнить серию рекурсивных поисков, пока не найдете начало и конец.

  1. Найдите индекс с помощью бинарного поиска.
  2. Бинарный поиск 1..(index-1) для значения.
  3. Если вы найдете значение, вам нужно будет снова выполнить поиск между 1 и newindex-1.
  4. Вам нужно будет повторять этот поиск, пока вы больше не найдете значение.
  5. Двоичный поиск (индекс+1)..конец значения.
  6. Если вы найдете значение, вам нужно будет снова выполнить поиск между newindex+1 и end.
  7. Вам нужно будет повторять этот поиск, пока вы больше не найдете значение.

Пример кода будет выглядеть примерно так. Этот код предназначен для бинарного поиска, который завершается при первом обнаружении совпадения.

function GetIndexes(const aSearch: TSearchIntegers; const aValue: Integer; var aStartIndex, aEndIndex: Integer): Boolean;
var
  foundIndex: Integer;
  lookFor: Integer;
begin
  if BinarySearch(aSearch, aValue, foundIndex) then
  begin
    Result := True;
    lookFor := foundIndex;
    repeat
      aStartIndex := lookFor;
    until not BinarySearch(aSearch, aValue, lookFor, TComparer<Integer>.Default, 1, aStartIndex - 1);
    lookFor := foundIndex;
    repeat
      aEndIndex := lookFor;
    until not BinarySearch(aSearch, aValue, lookFor, TComparer<Integer>.Default, aEndIndex + 1, High(aSearch) - aEndIndex);
  end
  else
    Result := False;
end;

В конечном итоге ваши данные (которых у нас нет) определят для вас наилучший план действий.

Теперь немного усложним. Разновидность бинарного поиска, которую Delphi использует в TArray.BinarySearch, не завершается раньше, чем найдено совпадение. Он всегда будет находить индекс первого элемента, поскольку он не выходит из цикла, когда находит совпадение.

Result := False;
L := Index;
H := Index + Count - 1;
while L <= H do
begin
  mid := L + (H - L) shr 1;
  cmp := Comparer.Compare(Values[mid], Item);
  if cmp < 0 then
    L := mid + 1
  else
  begin
    H := mid - 1;
    if cmp = 0 then
      Result := True;  // <-- It doesn't end here
  end;
end;

Это означает, что у вас есть небольшой штраф, когда у вас много одинаковых значений, но у него есть приятный побочный эффект. Вы можете сделать что-то вроде этого, чтобы найти то, что вы ищете:

function GetIndexes(const aSearch: TSearchIntegers; const aValue: Integer; var aStartIndex, aEndIndex: Integer): Boolean;
begin
  Result := False;
  if TArray.BinarySearch<Integer>(aSearch, aValue, aStartIndex) then
  begin
    TArray.BinarySearch<Integer>(aSearch, aValue+1, aEndIndex);
    if aSearch[aEndIndex] <> aValue then
      Dec(aEndIndex);
    Result := True;
  end;
end;

Это работает, потому что поиск также возвращает индекс следующего значения, даже если он не находит aValue + 1 в массиве. Оператор if в конце предназначен для обработки случая, когда наше значение также является последним значением массива.

Это зависит от кода для TArray.BinarySearch, оставшегося без изменений.

person Graymatter    schedule 06.07.2014
comment
@user3764855 user3764855 Кажется, все в порядке. Я не смотрел на это слишком подробно. Это немного похоже на излишество, когда в текущей реализации уже есть код, который делает то, что вам нужно (последний блок). Единственная проблема в том, что это может измениться в будущем. Все зависит от того, какие данные вы ожидаете и сколько дубликатов вы ожидаете найти. Это должно работать, но если вы никогда не ожидаете более одного или двух дубликатов, то это определенно излишне. - person Graymatter; 06.07.2014