Что такое бинарный поиск в программировании
Перейти к содержимому

Что такое бинарный поиск в программировании

  • автор:

Двоичный, или бинарный, поиск элемента

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

При решении задачи на языке программирования Python вместо массива используется список

Описание алгоритма

  1. Находится средний элемент последовательности. Для этого первый и последний индексы связываются с переменными, а индекс среднего элемента вычисляется.
  2. Значение среднего элемента сравнивается с искомым значением. Если значение среднего элемента оказывается равным искомому, поиск завершается.
  3. Иначе, в зависимости от того, искомое значение больше или меньше значения среднего элемента, дальнейший поиск будет происходить только в левой или только в правой половинах массива.
  4. Для этого одна из границ исследуемой последовательности сдвигается. Если искомое значение больше значения среднего элемента, то нижняя граница сдвигается за средний элемент на один элемент справа. Если искомое значение меньше значения среднего элемента, то верхняя граница сдвигается на элемент перед средним.
  5. Снова находится средний элемент теперь уже в выбранной половине. Описанный выше алгоритм повторяется для данного среза.

Исходный код на Python

from random import randint # Создание списка, # его сортировка по возрастанию # и вывод на экран a = [] for i in range(10): a.append(randint(1, 50)) a.sort() print(a) # искомое число value = int(input()) mid = len(a) // 2 low = 0 high = len(a) - 1 while a[mid] != value and low  high: if value > a[mid]: low = mid + 1 else: high = mid - 1 mid = (low + high) // 2 if low > high: print("No value") else: print("ID =", mid)
[6, 17, 21, 27, 32, 35, 35, 36, 37, 48] 27 ID = 3
[4, 5, 12, 13, 13, 18, 22, 26, 30, 35] 28 No value

Другие варианты решения задачи двоичного поиска на Python:

from random import randint a = [randint(1, 50) for i in range(10)] a.sort() print(a) value = int(input()) left = 0 right = len(a) - 1 center = (left + right) // 2 while a[center] != value: if value > a[center]: left = center + 1 else: right = center - 1 center = (left + right) // 2 if left >= right: break if value == a[center]: print("ID =", center) else: print("No value") 
from random import randint a = [randint(1, 50) for i in range(10)] a.sort() print(a) value = int(input()) left = 0 right = len(a) - 1 while left  right: center = (left + right) // 2 if value == a[center]: print("ID =", center) break if value > a[center]: left = center + 1 else: right = center - 1 else: print("No value") 

Блок-схема алгоритма бинарного поиска

X Скрыть Наверх

Решение задач на Python

Целочисленный двоичный поиск

Целочисленный двоичный поиск (бинарный поиск) (англ. binary search) — алгоритм поиска объекта по заданному признаку в множестве объектов, упорядоченных по тому же самому признаку, работающий за логарифмическое время.

Задача:
Пусть нам дан упорядоченный массив, состоящий только из целочисленных элементов. Требуется найти позицию, на которой находится заданный элемент.

Схема бинарного поиска

Принцип работы

Двоичный поиск заключается в том, что на каждом шаге множество объектов делится на две части и в работе остаётся та часть множества, где находится искомый объект. Или же, в зависимости от постановки задачи, мы можем остановить процесс, когда мы получим первый или же последний индекс вхождения элемента. Последнее условие — это левосторонний/правосторонний двоичный поиск.

Правосторонний/левосторонний целочисленный двоичный поиск

Для простоты дальнейших определений будем считать, что [math]a[-1] = -\infty[/math] и что [math]a[n] = +\infty[/math] (массив нумеруется с [math]0[/math] ).

Определение:
Правосторонний бинарный поиск (англ. rightside binary search) — бинарный поиск, с помощью которого мы ищем [math] \max\limits_ \ [/math] , где [math]a[/math] — массив, а [math]x[/math] — искомый ключ
Определение:
Левосторонний бинарный поиск (англ. leftside binary search) — бинарный поиск, с помощью которого мы ищем [math] \min\limits_\ [/math] , где [math]a[/math] — массив, а [math]x[/math] — искомый ключ

Использовав эти два вида двоичного поиска, мы можем найти отрезок позиций [math][l,r][/math] таких, что [math]\forall i \in [l,r] : a[i] = x[/math] и [math] \forall i \notin [l,r] : a[i] \neq x [/math]

Пример:

Задан отсортированный массив [math][1, 2, 2, 2, 2, 3, 5, 8, 9, 11], x = 2[/math] .

Правосторонний поиск двойки выдаст в результате [math]4[/math] , в то время как левосторонний выдаст [math]1[/math] (нумерация с нуля).

Отсюда следует, что количество подряд идущих двоек равно длине отрезка [math][1;4][/math] , то есть [math]4[/math] .

Если искомого элемента в массиве нет, то правосторонний поиск выдаст максимальный элемент, меньший искомого, а левосторонний наоборот, минимальный элемент, больший искомого.

Алгоритм двоичного поиска

Идея поиска заключается в том, чтобы брать элемент посередине, между границами, и сравнивать его с искомым. Если искомое больше(в случае правостороннего — не меньше), чем элемент сравнения, то сужаем область поиска так, чтобы новая левая граница была равна индексу середины предыдущей области. В противном случае присваиваем это значение правой границе. Проделываем эту процедуру до тех пор, пока правая граница больше левой более чем на [math]1[/math] .

Код

int binSearch(int[] a, int key): // Запускаем бинарный поиск int l = -1 // l, r — левая и правая границы int r = len(a) while l < r - 1 // Запускаем цикл m = (l + r) / 2 // m — середина области поиска if a[m] < key l = m else r = m // Сужение границ return r

Инвариант цикла: правый индекс не меньше искомого элемента, а левый — строго меньше (т.к значение [math]m[/math] присваевается левой границе [math]l[/math] , только тогда, когда [math]a[m][/math] строго меньше искомого элемента [math]key[/math] ), тогда если [math]r = l + 1[/math] (что эквивалентно [math]r-l=1[/math] ), то понятно, что [math]r[/math] — самое левое вхождение искомого элемента (так как предыдущие элементы уже меньше искомого элемента)

В случае правостороннего поиска изменится знак сравнения при сужении границ на [math]a[m] \leqslant k[/math] , а возвращаемой переменной станет [math]l[/math] .

Несколько слов об эвристиках

Эвристика с завершением поиска, при досрочном нахождении искомого элемента

Заметим, что если нам необходимо просто проверить наличие элемента в упорядоченном множестве, то можно использовать любой из правостороннего и левостороннего поиска. При этом будем на каждой итерации проверять «не попали ли мы в элемент, равный искомому», и в случае попадания заканчивать поиск.

Эвристика с запоминанием ответа на предыдущий запрос

Пусть дан отсортированный массив чисел, упорядоченных по неубыванию. Также пусть запросы приходят в таком порядке, что каждый следующий не меньше, чем предыдущий. Для ответа на запрос будем использовать левосторонний двоичный поиск. При этом после того как обработался первый запрос, запомним чему равно [math]l[/math] , запишем его в переменную [math]startL[/math] . Когда будем обрабатывать следующий запрос, то проинициализируем левую границу как [math]startL[/math] . Заметим, что все элементы, которые лежат не правее [math]startL[/math] , строго меньше текущего искомого элемента, так как они меньше предыдущего запроса, а значит и меньше текущего. Значит инвариант цикла выполнен.

Применение двоичного поиска на некоторых неотсортированных массивах

Задача:
Пусть отсортированный по возрастанию массив из [math]n[/math] элементов [math]a[0 \ldots n — 1][/math] , все элементы которого различны, был циклически сдвинут, требуется максимально быстро найти элемент в таком массиве.

Если массив, отсортированный по возрастанию, был циклически сдвинут, тогда полученный массив состоит из двух отсортированных частей. Используем двоичный поиск, чтобы найти индекс последнего элемента левой части массива. Для этого в реализации двоичного поиска заменим условие в if на [math]a[m] \gt a[n-1][/math] , тогда в [math]l[/math] будет содержаться искомый индекс:

int l = -1 int r = len(a) while l < r - 1 // С помощью бинарного поиска найдем максимум на массиве m = (l + r) / 2 // m — середина области поиска if a[m] > a[n - 1] // Сужение границ l = m else r = m int x = l // x — искомый индекс. 

Затем воспользуемся двоичным поиском искомого элемента [math]key[/math] , запустив его на той части массива, в которой он находится: на [math][0, x][/math] или на [math][x + 1, n — 1][/math] . Для определения нужной части массива сравним [math]key[/math] с первым и с последним элементами массива:

if key > a[0] // Если key в левой части l = -1 r = x + 1 if key < a[n] // Если key в правой части l = x r = n

Время выполнения данного алгоритма — [math]O(2\log n)=O(\log n)[/math] .

Задача:
Массив образован путем приписывания в конец массива, отсортированного по возрастанию, массива, отсортированного по убыванию. Требуется максимально быстро найти элемент в таком массиве.

Найдем индекс последнего элемента массива, отсортированного по возрастанию, воспользовавшись троичным поиском, затем запустим левосторонний двоичный поиск для каждого массива отдельно: для элементов [math][0 \ldots x][/math] и для элементов [math][x+1 \ldots n][/math] , где в качестве [math]x[/math] мы возьмем индекс максимума, найденный троичным поиском. Для массива, отсортированного по убыванию используем двоичный поиск, изменив условие в if на [math]a[m] \gt key[/math] .

Время выполнения алгоритма — [math] O(\log n)[/math] (так как и бинарный поиск, и тернарный поиск работают за логарифмическое время с точностью до константы).

Задача:
Два отсортированных по возрастанию массива записаны один в конец другого. Требуется максимально быстро найти элемент в таком массиве.

Мы имеем массив, образованный из двух отсортированных подмассивов, записанных один в конец другого. Запустить сразу бинарный или тернарный поиски на таком массиве нельзя, так как массив не будет обязательно отсортированным и он не будет иметь [math]1[/math] точку экстремума. Поэтому попробуем найти индекс последнего элемента левого массива, чтобы потом запустить бинарный поиск два раза на отсортированных массивах.

Докажем, что найти этот индекс невозможно быстрее, чем за [math]\Omega (n)[/math] . Возьмем возрастающий массив целых чисел, начиная с [math]1[/math] . Он удовлетворяет условию задачи. Вставим в него [math]0[/math] на любую позицию. Такой массив по-прежнему будет удовлетворять условию задачи. Следовательно, из-за того, что [math]0[/math] может находиться на любой позиции, мы можем его найти лишь за [math]\Omega (n)[/math] .

Задача:
Массив образован путем циклического сдвига массива, образованного приписыванием отсортированного по убыванию массива в конец отсортированного по возрастанию. Требуется максимально быстро найти элемент в таком массиве.

После циклического сдвига мы получим массив [math]a[0 \ldots n-1][/math] , образованный из трех частей: отсортированных по возрастанию-убыванию-возрастанию ( [math]\nearrow \searrow \nearrow [/math] ) или по убыванию-возрастанию-убыванию ( [math] \searrow \nearrow \searrow [/math] ). С помощью двоичного поиска найдем индексы максимального и минимального элементов массива, заменив условие в if на [math]a[m] \gt a[m — 1][/math] (ответ будет записан в [math]l[/math] ) или на [math]a[m] \gt a[m + 1][/math] (ответ будет записан в [math]r[/math] ) соответственно.

Фактически, при поиске индексов минимума и максимума мы проверяем, возрастает или убывает массив на промежутке [math] [ m — 1 ; m ] [/math] , а затем, в зависимости от того, что мы ищем, мы либо поднимаемся, либо опускаемся по этому промежутку возрастания (убывания). Однако при таком решении могут быть неправильно найдены значения минимума или максимума. Рассмотрим случаи, когда они будут неправильно найдены. Определить, какого вида наш массив возможно, сравнив первые два элемента массива.

Рассмотрим отдельно ситуацию, если наш массив вида возрастание-убывание-возрастание ( [math]\nearrow \searrow \nearrow [/math] ). В таком случае может быть неправильно найдено значение максимума, если последний промежуток возрастания занимает больше половины массива (мы будем подниматься по последнему промежутку возрастания вплоть до конца массива и за максимум будет принят последний элемент массива, что не всегда верно). Тогда, если последний элемент массива меньше первого, нужно еще раз запустить поиск максимума, но уже на промежутке от [math]0[/math] до [math]min[/math] , потому что истинный максимум будет находиться в первой точке экстремума, которую мы таким образом и найдем.

В случае же убывание-возрастание-убывание ( [math] \searrow \nearrow \searrow [/math] ) может быть, что будет неправильно найден минимум. Найдем правильный минимум аналогично поиску максимума в предыдущем абзаце.

Затем, в зависимости от типа нашего массива, запустим бинарный поиск три раза на каждом промежутке.

Время выполнения данного алгоритма — [math]O(6\log n)=O(\log n)[/math] .

Переполнение индекса середины

В некоторых языках программирования присвоение m = (l + r) / 2 приводит к переполнению. Вместо этого рекомендуется использовать m = l + (r — l) / 2; или эквивалентные выражения. [1]

См. также

  • Вещественный двоичный поиск
  • Троичный поиск
  • Поиск с помощью золотого сечения
  • Интерполяционный поиск

Источники информации

  • Д. Кнут — Искусство программирования (Том 3, 2-е издание)
  • Википедия — двоичный поиск
  • Типичные ошибки при написании бинарного поиска
  • Бинарный поиск на algolist

Бинарный поиск

Бинарный поиск — тип поискового алгоритма, который последовательно делит пополам заранее отсортированный массив данных, чтобы обнаружить нужный элемент. Другие его названия — двоичный поиск, метод половинного деления, дихотомия.

«IT-специалист с нуля» наш лучший курс для старта в IT

Принцип работы алгоритма бинарного поиска

Основная последовательность действий алгоритма выглядит так:

алгоритм бинарного поиска

  1. Сортируем массив данных.
  2. Делим его пополам и находим середину.
  3. Сравниваем срединный элемент с заданным искомым элементом.
  4. Если искомое число больше среднего — продолжаем поиск в правой части массива (если он отсортирован по возрастанию): делим ее пополам, повторяя пункт 3. Если же заданное число меньше — алгоритм продолжит поиск в левой части массива, снова возвращаясь к пункту 3.

Профессия / 8 месяцев
IT-специалист с нуля

Попробуйте 9 профессий за 2 месяца и выберите подходящую вам

vsrat_7 1 (1)

Реализация бинарного поиска

Существуют два способа реализации бинарного поиска.

1. Итерационный метод. При таком подходе используется цикл, тело которого повторяется, пока не найдется заданный элемент либо не будет установлено, что его нет в массиве. Например, в Python для этой цели удобно использовать цикл while.

2. Рекурсивный подход. В этом случае пишется функция, которая вызывает сама себя (рекурсивно), пока не будет найден искомый элемент в массиве.

бинарный поиск позиции заданного элемента в списке

Приведем примеры реализации этих методов на Python.

Пусть есть массив чисел [5, 8, 9, 1, 23, 7, 3, 0, 15], и необходимо найти позицию числа 5 в упорядоченном списке. На вход такой функции необходимо подать уже отсортированный массив, для этого воспользуемся встроенным методом sorted(), который упорядочивает массив данных по возрастанию.

Код, использующий итерационный подход, будет выглядеть так:

def findPosition(num_list, number):
first = 0
last = len(num_list) - 1
while first middle = first + (last - first) // 2
if num_list[middle] == number:
return middle
elif num_list[middle] < number:
first = middle + 1
else:
last = middle - 1
return -1
num_list = sorted([5, 8, 9, 1, 23, 7, 3, 0, 15])
number = 5
print(findPosition(num_list, number))

При использовании рекурсивного поиска код на Python можно написать так:

def findPosition(num_list, number, first, last): if last >= first: middle = first + (last - first) // 2 if num_list[middle] == number: return middle elif num_list[middle] < number: return findPosition(num_list, number, middle + 1, last) else: return findPosition(num_list, number, first, middle - 1) else: return -1 num_list = sorted([5, 8, 9, 1, 23, 7, 3, 0, 15]) number = 5 print(findPosition(num_list, number, 0, len(num_list) - 1))

Начните карьеру в Data Science.
Онлайн-магистратура МФТИ с практикой на реальных проектах

В некоторых языках программирования, включая Python, есть готовые функции для выполнения бинарного поиска. Модуль бинарного поиска называется bisect. Проиллюстрируем его работу на примере:

from bisect import bisect_left def findPosition(num_list, number): pos = bisect_left(num_list, number) if pos < len(num_list): return pos return False num_list = sorted([5, 8, 9, 1, 23, 7, 3, 0, 15]) number = 5 print(findPosition(num_list, number))

Читайте также Как выбрать IT-специальность в новых реалиях?

В каких случаях используют бинарный поиск

Двоичный поиск подходит для нахождения позиций элемента в упорядоченном списке: в этом случае он эффективнее линейного, поскольку массив данных на каждом шаге разделяется надвое и одна половина сразу отбрасывается. Последовательная сложность двоичного метода в худшем и среднем случаях равна O(log n), в лучшем — O(1) (если обнаруживаем искомый элемент на первой итерации). Для сравнения: вычислительная сложность линейного поиска равна O(n) (обычный проход по всем элементам в поисках нужного).

У бинарного поиска есть недостаток — он требует упорядочивания данных по возрастанию. Сложность сортировки — не менее O(n log n). Поэтому, если список короткий, используется все-таки линейный поиск.

Разновидности алгоритма

На основе бинарного поиска разработано несколько дополнительных разновидностей поисковых алгоритмов:

  • однородный бинарный поиск, при котором аргумент поиска А сравнивается с ключом Ki, где i — золотое сечение интервала (оно выбирается так, чтобы отношение длины большего отрезка к длине всего интервала равнялось отношению длины меньшего отрезка к длине большего отрезка);
  • троичный поиск, когда интервал делится на три части вместо двух. Обычно применяется для поиска положения экстремума функции;
  • интерполирующий поиск, который предсказывает позицию нужного элемента на основе разницы значений. Эффективен, если элементы распределены достаточно равномерно;
  • дробный спуск — применяется для ускорения двоичного поиска в многомерных массивах данных, и другие.

Data Scientist

Дата-сайентисты решают поистине амбициозные задачи. Научитесь создавать искусственный интеллект, обучать нейронные сети, менять мир и при этом хорошо зарабатывать. Программа рассчитана на новичков и плавно введет вас в Data Science.

Бинарный поиск: зачем нужен и как реализовать

О чем речь? Бинарный поиск – это алгоритм поиска элемента в отсортированном массиве данных. Этот метод является довольно популярным в программировании и может быть реализован на разных языках: от С до Python.

На что обратить внимание? Несмотря на свою простоту, у бинарного поиска есть ряд сложностей в реализации. Даже опытные программисты часто ошибаются при работе с данным алгоритмом.

В статье рассказывается:

  1. Понятие бинарного поиска
  2. Принцип работы бинарного поиска на примере
  3. Реализация бинарного поиска в программировании
  4. Проблемы бинарного поиска

Пройди тест и узнай, какая сфера тебе подходит:
айти, дизайн или маркетинг.
Бесплатно от Geekbrains

Понятие бинарного поиска

Бинарным (или двоичным) называют поиск элемента упорядоченного множества через многократное деление этого множества пополам. Искомый элемент всегда будет оказываться в одной из двух частей. Поиск прекращается, когда обнаруживается совпадение граничного элемента между двумя разделенными блоками с заданным, или когда заданный элемент не обнаруживается вовсе.

Реализация этого метода возможна только применимо к отсортированным множествам. Последовательно разбивая такой массив данных на две части, алгоритм каждый раз ищет заданный элемент только в одной половине.

В целом метод бинарного поиска можно описать следующим образом. Сначала в возрастающем или убывающем множестве определяется среднее значение, после чего оно сравнивается с искомым. При совпадении заданного и центрального элемента поиск прекращается — элемент считается найденным. В случае несовпадения значений создается новый массив значений соответственно слева и справа от среднего, и процедура повторяется уже на данном массиве.

Принцип работы бинарного поиска на примере

Принцип достаточно прост. Множество данных предварительно сортируется (чаще всего по возрастанию). Затем для поиска конкретных элементов выполняется следующая последовательность действий:

  1. Вычисляется среднее значение массива.
  2. Значение полученного элемента сравнивается с искомым (ключом). Если оно меньше, дальнейший поиск для возрастающего массива выполняется слева от центрального элемента. В противном случае ключ ищется справа.
  3. В случае совпадения среднего значения с искомым поиск прекращается. Пользователю возвращается индекс совпавшего элемента.
  4. Дальнейшие итерации первых двух шагов повторяются вплоть до нахождения ключа.
  5. Если в результате очередного деления остался лишь один элемент, и он не совпадает с искомым, пользователю возвращается значение -1.

Для большей наглядности рассмотрим пример из реальной практики.

Многие программисты задаются вопросом, почему в таком методе нельзя просто отсекать половину массива при поиске ключа. Рассмотрим такую ситуацию.

  • Имеется массив данных: 1, 3, 6, 7, 13, 15, 16, 19, 24, 28, 29.
  • Нужно найти элемент со значением 19.
  • Центральный элемент равен 15.
  • Поскольку число 19 больше 15, дальнейший поиск выполняется в правой половине.
  • Таким образом, значение 19 ищется в новом массиве: 16, 19, 24, 28, 29.

Узнай, какие ИТ - профессии
входят в ТОП-30 с доходом
от 210 000 ₽/мес
Павел Симонов
Исполнительный директор Geekbrains

Команда GeekBrains совместно с международными специалистами по развитию карьеры подготовили материалы, которые помогут вам начать путь к профессии мечты.

Подборка содержит только самые востребованные и высокооплачиваемые специальности и направления в IT-сфере. 86% наших учеников с помощью данных материалов определились с карьерной целью на ближайшее будущее!

Скачивайте и используйте уже сегодня:

Павел Симонов - исполнительный директор Geekbrains

Павел Симонов
Исполнительный директор Geekbrains

Топ-30 самых востребованных и высокооплачиваемых профессий 2023

Поможет разобраться в актуальной ситуации на рынке труда

Подборка 50+ бесплатных нейросетей для упрощения работы и увеличения заработка

Только проверенные нейросети с доступом из России и свободным использованием

ТОП-100 площадок для поиска работы от GeekBrains

Список проверенных ресурсов реальных вакансий с доходом от 210 000 ₽

Получить подборку бесплатно
Уже скачали 25963

Последовательность действий во время второй и последующих итераций до обнаружения ключа:

  • Серединой нового множества является число 24.
  • Поскольку 19 меньше 24, работа выполняется в левой половине данного массива.
  • Новая последовательность содержит всего пару чисел — 16 и 19.
  • Так как 19 больше 16, обрабатывать нужно элементы правее.
  • А справа находится лишь одно значение, которое равно искомому — 19.
  • Таким образом, ключ найден.

Мы привели простой пример, содержащий небольшое число шагов до обнаружения искомого элемента. Итого, за 4 итерации удалось обнаружить ключ в последовательности из 11 чисел.

Реализация бинарного поиска в программировании

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

int bin_find(int n, int *x, long A)

int m, left, right;

left = 0; right = n-1;

if (left > right) return (-1); // ключ не найден

m = left + (right — left) / 2;

if (x[m] > A) right = m — 1;

if (x[m] == A) return m;

В данном случае три индексные переменные lk, mk, rk сгруппированы в одну, так как в алгоритме используются лишь последнее значение числового ряда.

Реализация последовательного алгоритма двоичного поиска

Если поиск требуемого элемента не увенчался успехом, пользователю, как правило, возвращается значение -1. Но могут встречаться и другие варианты вывода, зависящие от условий использования метода:

В данном случае выводится значение, вывод которого в случае успешного поиска невозможен. Например, алгоритм возвращает результат вычисления (lk+1). Здесь k — это номер шага, выполняемого перед тем, как будет нарушено условие lk < rk. Таким образом пользователю будет передаваться информация о предполагаемом местонахождении ключа. Этот результат в дальнейшем может быть использован, к примеру, для вставки нового элемента в найденное место множества, оставляя массив упорядоченным.

Для вас подарок! В свободном доступе до 28.01 -->
Скачайте ТОП-10
бесплатных нейросетей
для программирования
Помогут писать код быстрее на 25%
Чтобы получить подарок, заполните информацию в открывшемся окне

  • Специальная константа

Назначается конкретным языком программирования. В частности, C# содержит константу null и требует использования типа «int?» для результата вместо стандартного «int». Аналогом в Python является «…».

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

Несмотря на сравнительную простоту исходного кода, в нем все же могут возникнуть ловушки. Поэтому их следует учитывать:

  • Первая и последняя переменные массива по отдельности вмещаются в максимальный размер типа данных, а их сумма уже выходит за разрешенные пределы.

В теории массивы такого большого размера могут возникать. При этом программистам приходится внедрять конструкции вида «first + (last — first) / 2», гарантированно не приводящим к переполнению. Здесь первая и последняя переменная должны быть неотрицательными целыми числами.

  • Упомянутые элементы first и last являются указателями либо итераторами.

В таком случае приведенный выше код будет единственно корректным. Если преобразовать его в uintptr_t и провести последующий расчет, это нарушит абстракцию. При этом корректный результат не будет гарантирован. Для сохранения сложности алгоритма необходимо внедрять быстрые операции «итератор+число → итератор», «итератор−итератор → число».

  • Первый и последний элементы массива имеют тип данных со знаком.

Расчет производится с переводом в беззнаковый тип: ((unsigned)first + (unsigned)last) / 2. На языке Java это будет иметь вид: (first + last) >>> 1.

  • Требуется выполнить расчет на ассемблере, используя флаг переноса.

Используется конструкция вида add eax, b; rcr eax, 1. Но для длинных типов данных более целесообразным вариантом будет first + (last — first) / 2.

Дарим скидку от 60%
на обучение «Аналитик больших данных» до 28 января
Уже через 9 месяцев сможете устроиться на работу с доходом от 150 000 рублей

В процессе бинарного поиска элементов довольно часто случаются ошибки на единицу. По этой причине особую важность имеет предварительное тестирование подобных случаев. Например, в пустом массиве производится поиск отсутствующего значения (слишком большого, слишком малого и среднего). Также ищутся крайние элементы множества. Далее проверяют, не вышел ли алгоритм за границы массива и не попал ли он в бесконечный цикл.

Иногда множество содержит несколько экземпляров искомого элемента и нужно найти, например, самый первый (последний) экземпляр или даже следующий за первым (или последним) экземпляром элемент.

Ученый Йон Бентли заметил, что 9 из 10 начинающих разработчиков, создавая алгоритм двоичного поиска, попросту не берут в расчет указанные выше особенности. Даже код самого Бентли, опубликованный в качестве примера в нескольких учебных изданиях, обладает недостатком, связанным с возможным переполнением.

Только до 1.02
Скачай подборку материалов, чтобы гарантированно найти работу в IT за 14 дней
Список документов:

ТОП-100 площадок для поиска работы от GeekBrains

20 профессий 2023 года, с доходом от 150 000 рублей

Чек-лист «Как успешно пройти собеседование»

Чтобы зарегистрироваться на бесплатный интенсив и получить в подарок подборку файлов от GeekBrains, заполните информацию в открывшемся окне

Реализация параллельного алгоритма двоичного поиска

Алгоритм можно модифицировать внедрением p-ого поиска, позволяющего заранее задавать любое количество процессоров (p). В таком случае множество делится на p+1 частей, а не пополам. Все новые узлы при этом вычисляются и сравниваются с ключом независимо друг от друга (параллельно). Примерная высота ЯПФ определяется по формуле K=lg(p)+1n.

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

Реализовывать алгоритм двоичного поиска для архитектур с разделенной памятью в любом случае бессмысленно. Нелокальный доступ к исходным данным, а также сравнительно малое число вычислительных операций даже в условиях распараллеливания сводят всю эффективность на нет, так как извлекаемая выгода будет поглощаться коммуникациями.

В теории целесообразно использовать системы с общей памятью путем внедрения OpenMP. Но чтобы получить хотя бы какой-то выигрыш в скорости, необходимо выполнить ряд условий:

  • В память помещается весь массив исходных данных.
  • Программа начинает работу с однократного статического создания нитей.

Существенная же выгода распараллеливания будет возможна при условии многократного поиска в данном массиве. При этом внутри одной параллельной секции должно осуществляться хотя бы 1000 операций (или примерно 30 поисков в массиве из миллиарда элементов).

Проблемы бинарного поиска

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

Сложность бинарного поиска, несмотря на сравнительную простоту его алгоритма, заключается в самой практической реализации. Здесь полезно привести слова упомянутого выше Йона Бентли, которые он адресовал своим студентам:

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

Привлекает мир кодирования и создания программ? На курсе программиста с нуля до Junior вы освоите основы, познакомитесь с языками и инструментами разработки, и станете готовы к созданию своих первых проектов в IT-индустрии.

Итак, алгоритм бинарного поиска по своей сути простой, но при этом правильно реализовать его достаточно сложно. В книге Дональда Кнута «Sorting and Searching» говорится о первой публикации идеи двоичного поиска. Впервые принцип был опубликован в 1946 году, но лишь спустя 12 лет этот принцип грамотно реализовали в виде кода программы.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *