* docs(ru): replace prose quotes with guillemets * docs(ru): replace prose semicolons with periods * docs(ru): align animation title forms * docs(ru): align figure and table references
8.9 KiB
Двоичный поиск
Двоичный поиск (binary search) - это эффективный алгоритм поиска, основанный на стратегии «разделяй и властвуй». Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет.
!!! question
Дан массив `nums` длины $n$, элементы которого расположены в порядке возрастания и не повторяются. Найдите и верните индекс элемента `target` в этом массиве. Если массив не содержит этого элемента, верните $-1$ . Пример показан на рисунке ниже.
Как показано на рисунке ниже, сначала инициализируем указатели i = 0 и j = n - 1 , которые указывают на первый и последний элементы массива и задают интервал поиска [0, n - 1] . Обратите внимание: квадратные скобки обозначают замкнутый интервал и включают граничные значения.
Далее в цикле выполняются следующие два шага.
- Вычислить индекс середины
m = \lfloor {(i + j) / 2} \rfloor, где\lfloor \: \rfloorозначает операцию округления вниз. - Сравнить
nums[m]иtarget, после чего возможны три случая.- Если
nums[m] < target, это означает, чтоtargetнаходится в интервале[m + 1, j], поэтому выполняетсяi = m + 1. - Если
nums[m] > target, это означает, чтоtargetнаходится в интервале[i, m - 1], поэтому выполняетсяj = m - 1. - Если
nums[m] = target, значит, элементtargetнайден, поэтому возвращается индексm.
- Если
Если массив не содержит целевой элемент, область поиска в итоге сузится до пустого интервала. В этом случае возвращается -1 .
Стоит отметить, что поскольку и i , и j имеют тип int , то сумма i + j может выйти за пределы диапазона типа int. Чтобы избежать переполнения, обычно используют формулу m = \lfloor {i + (j - i) / 2} \rfloor для вычисления середины.
Код приведен ниже:
[file]{binary_search}-[class]{}-[func]{binary_search}
Временная сложность равна $O(\log n)$ : в цикле двоичного поиска интервал каждый раз сокращается вдвое, поэтому число итераций равно \log_2 n .
Пространственная сложность равна $O(1)$ : указатели i и j занимают константный объем памяти.
Методы представления интервалов
Помимо описанного выше двойного замкнутого интервала, часто используется и левозамкнутый правооткрытый интервал, который задается как [0, n) , то есть левая граница включается, а правая - нет. В этом представлении интервал [i, j) пуст, когда i = j .
На основе этого представления можно реализовать двоичный поиск с той же функциональностью:
[file]{binary_search}-[class]{}-[func]{binary_search_lcro}
Как показано на рисунке ниже, в этих двух вариантах представления интервала различаются инициализация, условие цикла и операция сужения интервала в алгоритме двоичного поиска.
Поскольку в записи «двойной замкнутый интервал» обе границы являются закрытыми, операции сужения интервала при помощи указателей i и j тоже получаются симметричными. Из-за этого в таком варианте сложнее допустить ошибку, поэтому обычно рекомендуется использовать именно запись «двойной замкнутый интервал».
Преимущества и ограничения
Двоичный поиск показывает хорошие результаты и по времени, и по памяти.
- Двоичный поиск очень эффективен по времени. На больших объемах данных логарифмическая временная сложность дает заметное преимущество. Например, когда размер данных
n = 2^{20}, линейный поиск потребует2^{20} = 1048576итераций, тогда как двоичный поиск выполнится всего за\log_2 2^{20} = 20итераций. - Двоичный поиск не требует дополнительной памяти. По сравнению с алгоритмами поиска, которым нужно внешнее пространство (например, с хеш-поиском), двоичный поиск заметно экономнее по памяти.
Однако двоичный поиск подходит не для всех ситуаций, и основные причины таковы.
- Двоичный поиск применим только к упорядоченным данным. Если входные данные неупорядочены, специально сортировать их ради двоичного поиска невыгодно. Это связано с тем, что временная сложность алгоритмов сортировки обычно составляет
O(n \log n), что выше, чем у линейного и двоичного поиска. Если элементы приходится часто вставлять, то для сохранения порядка в массиве их нужно помещать в конкретные позиции, а это требуетO(n)времени и тоже обходится дорого. - Двоичный поиск применим только к массивам. Для него нужен скачкообразный доступ к элементам, а в связном списке такой доступ малоэффективен, поэтому двоичный поиск не подходит для списков и структур данных, построенных на их основе.
- При малом объеме данных линейный поиск работает лучше. В линейном поиске на каждом шаге нужна всего одна операция сравнения. В двоичном поиске требуется 1 сложение, 1 деление, от 1 до 3 сравнений и еще 1 сложение или вычитание, то есть всего от 4 до 6 элементарных операций. Поэтому при небольшом
nлинейный поиск может оказаться быстрее двоичного.








