Бинарный поиск (binary search) находит элемент в отсортированном массиве, на каждом шаге отбрасывая половину: вместо у перебора. Та же идея работает для монотонной функции — это бинарный поиск по ответу.
Зачем нужно
Проверить, есть ли key в массиве, можно перебором — линейным поиском:
flag = false
for i = 0 ... N
если arr[i] = key
flag = true
вывести(i)
break
если flag = false
вывести("не нашёл"), . Быстрее нельзя, пока о массиве ничего не известно. Если же массив отсортирован — — одно сравнение с серединой сразу говорит, в какой половине искать.
При : перебор — итераций, около секунд; бинарный поиск — итераций.
Граница в линейном поиске
Индексы — от 0 до . Если
for i = 0 ... Nвключает N, это обращение за пределы массива, в C++ — Неопределённое поведение.
Как работает
- ⇒ искомое в левой половине;
- ⇒ в правой.
L = 0
R = N - 1
пока (R - L > 1) {
M = (L + R) / 2 // в C++: M = L + (R - L) / 2
если arr[M] > key
R = M
иначе если arr[M] < key
L = M
иначе
L = M
break
}
если arr[L] = key или arr[R] = key
вывести("нашли")
иначе
вывести("не нашли")Цикл сужает отрезок, пока в нём больше двух элементов. Оставшиеся два соседних кандидата проверяются после цикла.
Не из лекции: доказательство правильности
Инвариант: если
keyесть в массиве, он среди .
- Вначале это весь массив.
arr[M] > key: из сортировки при , искомое в .arr[M] < key: симметрично, в .arr[M] = key: найден,L = M, проверка после цикла его увидит.Остановка: при середина строго внутри, , и строго уменьшается.
Ответ: после цикла — кандидатов только два, и по инварианту искомое, если оно есть, среди них.
Асимптотика
Длина отрезка: . Остановка при , то есть : , .
Не из лекции: аккуратно
При части отрезка — и , новое . Шагов до — не больше . Почему основание логарифма в оценке не пишут — см. Асимптотические оценки.
Почему L + (R − L) / 2
На лекции отмечено: в C++ середину пишут как M = L + (R − L) / 2.
Не из лекции: причина
L + Rпри индексах около не помещается вint(максимум ). Переполнение знакового целого — UB, см. Неопределённое поведение и Целочисленные типы.R − Lлежит между 0 и R и переполниться не может. Для результат тот же: .
Бинарный поиск по ответу
Обычный бинарный поиск ищет число в массиве, поиск по ответу — число, но в функции. Если монотонна на , её значения упорядочены, как отсортированный массив: . Середина отрезка по и сравнение с целью говорят, в какой половине ответ.
Пример:
. Функция монотонна при .
float L = 0, R = N
eps = 10^-5
пока (|L*L - N| > eps) {
M = (L + R) / 2
если M*M < N
L = M
иначе
R = M
}Второй вариант — с гарантированным числом итераций:
float L = 0, R = N
for i = 0 ... 1000
M = (L + R) / 2
если M*M < N
L = M
иначе
R = MНе из лекции: инвариант и границы
Инвариант: , то есть . Каждая ветка сохраняет свою половину неравенства, отрезок уменьшается вдвое.
Граница годится только при : из следует . При корень больше самого числа (), и нужна .
Почему вариант с eps опасен на дробных числах и сколько итераций из 1000
реально работают — проверено в
конспекте лекции
и объяснено в Типы с плавающей точкой.
Тонкости
- Массив обязан быть отсортирован. На неотсортированном бинарный поиск не падает, а просто отвечает неверно.
- Пустой массив () даёт : проверка
arr[L]после цикла обратится к несуществующему элементу. Его проверяют до поиска. - Для поиска по ответу нужна монотонность. Без неё отбрасывание половины ничем не обосновано.
Частые ошибки
- Считать середину как
(L + R) / 2на больших индексах в C++. - Искать дробный ответ «до точности eps» на
float: цикл может не остановиться. - Не проверить после цикла оба оставшихся элемента — и
arr[L], иarr[R].
Углубление — АиСД L03
Бинарный поиск назван среди применений сортировки: чтобы искать за , массив сначала сортируют.
Не из лекции: когда это выгодно
Сортировка дороже одного линейного поиска. Выгода появляется, когда запросов много: отсортировать один раз, а потом отвечать на каждый запрос за вместо .
Расхождение с L02
Было (L02): «Индексы — от 0 до . Если
for i = 0 ... Nвключает N, это обращение за пределы массива».В L03 и L04: запись
0 ... Nв сортировках означает полуинтервал — иначе пузырёк сj = 0 ... N−i−1иarr[j+1]выходил бы за массив, а MergeSort вызывается как(arr, 0, N, buffer)дляarr[0; N). При таком чтении линейный поиск из L02 за массив не выходит.Но запись непостоянна: в решете Эратосфена (L03)
2 ... Nвключает N, в проверке на простоту (L01)1 ... pвключает p. Предупреждение в L02 верно только при включающем чтении. Решение — за пользователем.