Целочисленный двоичный поиск
Целочисленный двоичный поиск (бинарный поиск) (англ. binary search) — алгоритм поиска объекта по заданному признаку в множестве объектов, упорядоченных по тому же самому признаку, работающий за логарифмическое время.
Формулировка задачи
Пусть нам дан упорядоченный массив, состоящий только из целочисленных элементов. Требуется найти позицию, на которой находится заданный элемент. Для этой задачи мы и можем использовать двоичный поиск.
Принцип работы
Двоичный поиск заключается в том, что на каждом шаге множество объектов делится на две части и в работе остаётся та часть множества, где находится искомый объект. Или же, в зависимости от постановки задачи, мы можем остановить процесс, когда мы получим первый или же последний индекс вхождения элемента. Последнее условие — это левосторонний/правосторонний двоичный поиск.
Правосторонний/левосторонний целочисленный двоичный поиск
Для простоты дальнейших определений будем считать, что
и что .
Определение: |
Правосторонний бинарный поиск (англ. rightside binary search) — бинарный поиск, с помощью которого мы ищем | , где — массив, а — искомый ключ
Определение: |
Левосторонний бинарный поиск (англ. leftside binary search) — бинарный поиск, с помощью которого мы ищем | , где — массив, а — искомый ключ
Использовав эти два вида двоичного поиска, мы можем найти отрезок позиций таких, что и
Например:
Задан отсортированный массив
.Правосторонний поиск двойки выдаст в результате
, в то время как левосторонний выдаст (нумерация с единицы).От сюда следует, что количество подряд идущих двоек равно длине отрезка
, то есть .Если искомого элемента в массиве нет, то правосторонний поиск выдаст минимальный элемент, больший искомого, а левосторонний наоборот, максимальный элемент, меньший искомого.
Алгоритм двоичного поиска
Идея поиска заключается в том, чтобы брать элемент посередине, между границами, и сравнивать его с искомым. Если искомое больше(в случае правостороннего — не меньше), чем элемент сравнения, то сужаем область поиска так, чтобы новая левая граница была равна индексу середины предыдущей области. В противном случае присваиваем это значение правой границе. Проделываем эту процедуру до тех пор, пока правая граница больше левой более чем на
. В случае правостороннего бинарного поиска ответом будет индекс , а в случае левостороннего — .Код
int binSearch(int[] a, int key) // l, r - левая и правая границы int l = 0 int r = len(a) + 1 while l < r - 1 // запускаем цикл m = (l + r) / 2 // m — середина области поиска if a[m] < key l = m else r = m // сужение границ return r
В случае правостороннего поиска изменится знак сравнения при сужении границ на .
Инвариант цикла: пусть левый индекс не больше искомого элемента, а правый — строго больше, тогда если
, то понятно, что — самое правое вхождение (так как следующее уже больше).Несколько слов об эвристиках
Эвристика с завершением поиска, при досрочном нахождении искомого элемента
Заметим, что если нам необходимо просто проверить наличие элемента в упорядоченном множестве, то можно использовать любой из правостороннего и левостороннего поиска. При этом будем на каждой итерации проверять "не попали ли мы в элемент, равный искомому", и в случае попадания заканчивать поиск.
Эвристика с запоминанием ответа на предыдущий запрос
Пусть дан отсортированный массив чисел, упорядоченных по неубыванию. Также пусть запросы приходят в таком порядке, что каждый следующий не меньше, чем предыдущий. Для ответа на запрос будем использовать левосторонний двоичный поиск. При этом после того как обработался первый запрос, запомним чему равно
, запишем его в переменную . Когда будем обрабатывать следующий запрос, то проинициализируем левую границу как . Заметим, что все элементы, которые лежат не правее , строго меньше текущего искомого элемента, так как они меньше предыдущего запроса, а значит и меньше текущего. Значит инвариант цикла выполнен.Применение двоичного поиска на неотсортированных массивах
Применение поиска на циклически сдвинутом отсортированном массиве
Пусть отсортированный по возрастанию массив
int l = 0 int r = n + 1 while l < r - 1 // Запускаем цикл... m = (l + r) / 2 // m — середина области поиска. if a[m] > a[n] // Сужение границ.. l = m else r = m int x = l // x — искомый индекс.
Затем воспользуемся двоичным поиском искомого элемента
if key > a[0] // Если key в левой части... l = 0 r = x + 1 if key < a[n] // Если key в правой части... l = x + 1 r = n + 1
Время выполнения данного алгоритма —
.Применение поиска на массиве, отсортированном по возрастанию, в конец которого приписан массив, отсортированный по убыванию
Найдем индекс последнего элемента массива, отсортированного по возрастанию, воспользовавшись двоичным поиском, условие в
int l = 0 int r = n + 1 while l < r - 1 // Запускаем цикл... m = (l + r) / 2 // m — середина области поиска. if a[m] > a[m - 1] // Сужение границ... l = m else r = m int x = l // x — искомый индекс.
Затем запустим левосторонний двоичный поиск для каждого массива отдельно: для элементов
и для элементов . Для массива, отсортированного по убыванию используем двоичный поиск, измененнив условие в на .Время выполнения алгоритма —
.
Применение поиска на двух отсортированных по возрастанию массивах, записанных один в конец другого
Найдем индекс последнего элемента левого массива, заменив условие в
на . Так мы найдем единственный элемент в массиве, который меньше предыдущего элемента (все остальные элементы больше предыдущего, так как массивы отсортированы по возрастанию). Однако такой алгоритм не будет работать, если левый массив длиннее, чем правый, и будет равняться начальному значению . Поэтому будем запускать такой алгоритм рекурсивно, если равно начальному значению , беря за новое начальное значение середину массива. Таким образом мы запустим бинарный поиск еще раз, постоянно уменьшая массив в два раза. Тогда через итераций либо левый массив перестанет быть длиннее, чем правый, и значение окажется отличным от (начальное значение ), либо длина массива станет равной двум и рекурсия потеряет всякий смысл (если в таком случае останется равным , то, значит, что первый элемент правого массива больше последнего элемента левого массива. Тогда два массива будут являться одним массивом отсортированным по возрастанию). После выполнения этого алгоритма и будет номером последнего элемента из левого массива.
// Поиск последнего элемента левого массива function indexOfLastLeftArrayElement(int a,int b) int l = a int r = b while l < r - 1 // Запускаем цикл... m = (l + r) / 2 // m — середина области поиска. if a[m] < a[m - 1] // Сравнение с предыдущим... l = m else r = m int last = l if last == a and r - (a + b) / 2 > 1 indexOfLastLeftArrayElement((a + b) / 2, r)
Проверим, равен ли
. Если да, то запустим бинарный поиск на массиве от до . Иначе, зная номер последнего элемента левого массива, запустим два раза левосторонний бинарный поиск: на массиве от до , и на массиве от до .Время выполнения алгоритма —
(мы запускаем бинарный поиск, который требует времени раз.Применение поиска на циклически сдвинутом массиве, образованном приписыванием отсортированного по убыванию массива в конец отсортированного по возрастанию
После циклического сдвига мы получим массив
// Поиск максимума... int l = 0 int r = n + 1 while l < r - 1 // Запускаем цикл... m = (l + r) / 2 // m — середина области поиска. if a[m] > a[m - 1] // Сужение границ.. l = m else r = m int max = l
// Поиск минимума... int l = 0 int r = n + 1 while l < r - 1 // Запускаем цикл... m = (l + r) / 2 // m — середина области поиска. if a[m] > a[m + 1] // Сужение границ.. l = m else r = m int min = r
Затем, в зависимости от расположения частей (можно узнать, сравнив
и ), запустим двоичный поиск для каждой части отдельно аналогично задаче о поиске элемента на массиве, отсортированном по возрастанию, в конец которого приписан массив, отсортированный по убыванию.Время выполнения данного алгоритма —
.См. также
Источники информации
- Д. Кнут - Искусство программирования (Том 3, 2-е издание)
- Википедия - двоичный поиск
- Интересная статья про типичные ошибки
- Бинарный поиск на algolist