CS
Алгоритмы
Задачник Базовый курс EN

SRT Поиск и сортировка · Алгоритм

Двоичный поиск

Также: Binary search · бинарный поиск · дихотомия · lower_bound · git bisect

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

База 20 минут поиск двоичный поиск массив контроль версий классика собеседования

Сложность

Время
O(log n)
Память
O(1)

проверок не больше ⌈log₂(n + 1)⌉: для миллиона — 20, для миллиарда — 30

Знать заранее

O-большое

В базовом курсе

20. Турнир сортировок

Где работает

  • git bisect: поиск коммита с ошибкой
  • индексы баз данных и B-деревья — тот же приём внутри узла
  • bisect в Python и sort.Search в Go
  • отладка «работало — не работает» любого рода

История

Джон Мокли (первое упоминание), 1946

Содержание

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

Вчера всё работало, сегодня сломано, а между этими версиями сотни коммитов. Проверять каждый — долго: собрать, запустить, посмотреть. Но проверять каждый и не нужно. Возьмём середину: если там уже сломано, виноват кто-то в первой половине, если ещё нет — во второй. Каждая проверка отрезает половину подозреваемых. Ниже настоящая история этого сайта, 904 коммита с февраля по октябрь 2026 года. Выберите вопрос — например, когда появился учебник математики, — и посмотрите, сколько проверок понадобится.

Десять проверок на 904 коммита. Перебор от первого коммита до учебника математики сделал бы 484. На миллионе коммитов двоичному поиску хватило бы двадцати. Это команда git bisect: она так и работает, только проверку делаете вы — собираете версию, которую она выбрала, и говорите «good» или «bad».

Идея

Если ответ на вопрос «уже да?» вдоль ряда меняется ровно один раз — сначала идут «нет», потом только «да», — то границу можно найти, каждый раз спрашивая о середине оставшегося куска. Один ответ делит кусок пополам, и через log₂ n вопросов от него остаётся одна точка.

Обратите внимание: здесь ничего не сказано про числа и сортировку. Нужно только, чтобы вопрос был монотонным: если в коммите №483 учебник уже есть, то он есть и во всех следующих. Отсортированный массив — частный случай: вопрос «элемент ≥ x?» для него монотонен. А ещё монотонны «хватит ли k грузовиков?», «уложится ли программа в секунду при n элементах?», «сломан ли сайт в этой версии?» — и двоичный поиск работает для всех них. Последний случай подробно разобран в статье о двоичном поиске по ответу.

Нижняя и верхняя граница

Чаще всего двоичный поиск ищет не «есть ли x в массиве», а границу: первое место, где элемент не меньше x. Эту позицию называют нижней границей (lower bound). Первое место, где элемент строго больше x, — верхняя граница. Между ними лежат все элементы, равные x, а справа от нижней границы — все элементы, не меньшие x.

Вот все землетрясения силой от 5 баллов за 2015–2025 годы по каталогу USGS — 19 073 штуки, отсортированные по магнитуде. Сколько из них сильнее 7? Подвигайте порог и посмотрите, как lo и hi сходятся к ответу.

Подсвеченная полоса — это всё, о чём мы пока ничего не знаем. Код держит одно утверждение, инвариант: все элементы левее lo меньше порога, все элементы начиная с hi — не меньше. В начале lo = 0 и hi = n, и утверждение верно, потому что слева и справа пусто. Каждый шаг смотрит на середину и сдвигает одну из границ так, что утверждение остаётся верным. Когда lo и hi встретились, неизвестного не осталось, и lo — ответ.

Почему цикл правильный и конечный

Возьмём середину mid = ⌊(lo + hi)/2⌋. Пока lo < hi, она лежит в полуинтервале [lo, hi): mid ≥ lo и mid < hi.

  • Если a[mid] < x, то и все элементы левее mid меньше x — массив отсортирован. Сдвигаем lo = mid + 1: инвариант «левее lo всё меньше» по-прежнему верен.
  • Иначе a[mid] ≥ x, и все правее тоже. Сдвигаем hi = mid: «начиная с hi всё не меньше» по-прежнему верно.

В обоих случаях длина hi − lo строго уменьшается: в первом lo вырос как минимум на единицу, во втором hi стал меньше прежнего. Значит, цикл закончится, причём каждый раз длина уменьшается почти вдвое, поэтому шагов не больше ⌈log₂(n + 1)⌉.

Именно здесь прячутся почти все ошибки: напишите lo = mid вместо lo = mid + 1, и при hi = lo + 1 середина будет равна lo, длина перестанет уменьшаться, и цикл не кончится никогда.

Код

Обе границы на полуинтервале [lo, hi). Это самая надёжная запись: ответ n («такого элемента нет») получается сам собой, без особых случаев.

В стандартных библиотеках это уже есть. В Python — модуль bisect: bisect_left(a, x) — нижняя граница, bisect_right(a, x) — верхняя. В Go — sort.Search(n, f): он находит первый индекс, на котором функция f стала истинной, то есть работает с любым монотонным вопросом, а не только с массивом. Для срезов есть ещё slices.BinarySearch.

А это git bisect по истории сайта. Проверка коммита здесь — заготовленный ответ, а не сборка, но поиск тот же самый:

Ошибки, которые делают все

Джон Бентли в книге «Жемчужины программирования» рассказывал, как давал задачу написать двоичный поиск профессиональным программистам на курсах в Bell Labs и IBM. Времени было достаточно, а правильный код в итоге оказался примерно у каждого десятого. Ошибки повторяются из года в год:

  • Бесконечный цикл. lo = mid при mid, округлённом вниз: на отрезке из двух элементов поиск стоит на месте.
  • Смесь соглашений. Полуинтервал [lo, hi) в одном месте и отрезок [lo, hi] в другом: тогда то цикл пропускает последний элемент, то читает за концом массива. Выберите одно соглашение и держитесь его.
  • Переполнение. В Java и C (lo + hi) / 2 переполняется, когда lo + hi больше 2³¹ − 1. Такая ошибка девять лет жила в Arrays.binarySearch из стандартной библиотеки Java, пока в 2006 году о ней не написал Джошуа Блох. Надёжная запись — lo + (hi - lo) / 2. В Python целые не переполняются, в Go int 64-битный, но привычка полезная.
  • Неотсортированные данные. Двоичный поиск по неотсортированному массиву ничего не находит и ничего не сообщает об ошибке — просто возвращает чепуху. Если массив меняется, сортировать его заново или держать в дереве поиска.

Сколько это стоит

Каждая проверка — это ответ «да» или «нет», ровно один бит информации. Чтобы различить n + 1 возможный ответ, нужно не меньше log₂(n + 1) бит, поэтому двоичный поиск оптимален среди всех способов, которые узнают о данных только сравнениями. Памяти — пара переменных.

ЭлементовПроверок перебором (в худшем случае)Проверок двоичным поиском
904 коммита90410
19 073 землетрясения19 07315
миллион1 000 00020
миллиард1 000 000 00030

Есть и цена: данные должны быть упорядочены. Сортировка стоит O(n log n), и если искать нужно один раз, проще пройти массив подряд за O(n). Двоичный поиск выигрывает, когда поисков много, или когда порядок уже есть сам собой — как у коммитов во времени.

История

Идею двоичного поиска по отсортированной таблице упомянул Джон Мокли в лекциях школы Мура в 1946 году — одном из первых курсов программирования в истории. Но, как заметил Дональд Кнут в «Искусстве программирования», вариант, правильно работающий для таблицы любой длины, появился в печати на годы позже. Короткий алгоритм, который все понимают с первого взгляда, оказался одним из самых трудных для безошибочной записи.

Задачи

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

Землетрясения в диапазоне Корень k-й степени

Данные: История коммитов legost.in Землетрясения: каталог USGS, общественное достояние

Смотрите также

SRT Приём Двоичный поиск по ответу Если проверить ответ легче, чем найти, и проверка монотонна, — ищем ответ делением пополам. O(log D · стоимость проверки) 2 задачи SRT Алгоритм Экспоненциальный поиск Шаги 1, 2, 4, 8, … пока не перескочишь, потом двоичный поиск: когда граница неизвестна или ответ близко. 2⌊log₂ i⌋ + 1 вопрос до позиции i 1 задача SRT Алгоритм Интерполяционный поиск Прикинуть место по значениям на краях: log log n проб на равномерных ключах и до n — на неудобных. ≈ log₂ log₂ n в среднем, n в худшем 1 задача SRT Алгоритм Троичный поиск Вершина унимодальной функции: сравнить две точки и отбросить часть отрезка. И почему золотое сечение лучше. O(log(L/ε)) вычислений функции 2 задачи DS Структура данных Двоичное дерево поиска Меньшие налево, большие направо: поиск, порядок и k-й элемент за высоту дерева — от log n до n. O(h): от O(log n) до O(n) 2 задачи SRT Приём Нижние границы сравнений Почему нельзя быстрее: подсчёт исходов и игра против судьи; сортировка, второй в турнире, минимум с максимумом, слияние. сортировка — ⌈log₂ n!⌉ сравнений 1 задача
← В картотеку Каталог алгоритмов Следующая → Поиск и сортировка Двоичный поиск по ответу

Учебник пишет Вячеслав Легостин. Всё бесплатно; код читателей не сохраняется.

Computer Science Каталог алгоритмов Задачник Базовый курс Математика legost.in

Каталог алгоритмов

Содержание

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

Задачник

SRT Поиск и сортировка

  1. Двоичный поиск
  2. Двоичный поиск по ответу
  3. Экспоненциальный поиск
  4. Интерполяционный поиск
  5. Троичный поиск
  6. Простые сортировки
  7. Сортировка слиянием
  8. Timsort
  9. Внешняя сортировка
  10. Быстрая сортировка
  11. Пирамидальная сортировка
  12. Сортировка подсчётом и поразрядная
  13. Нижние границы сравнений
  14. Быстрый выбор
  15. Top-k: k лучших
  16. Перемешивание Фишера — Йетса

DS Структуры данных

  1. Динамический массив
  2. Связный список
  3. Стек и очередь
  4. Монотонный стек
  5. Хеш-таблица
  6. Хеширование кукушки
  7. Фильтр Блума
  8. Корневая декомпозиция
  9. Персистентные структуры
  10. Верёвка
  11. Двоичная куча
  12. Двоичное дерево поиска
  13. АВЛ-дерево
  14. Красно-чёрное дерево
  15. Декартово дерево
  16. Список с пропусками
  17. B-дерево
  18. Косое дерево
  19. Бор (префиксное дерево)
  20. Система непересекающихся множеств
  21. Дерево отрезков
  22. Дерево Фенвика
  23. Разреженная таблица
  24. LRU-кэш

GRA Графы

  1. Как хранить граф
  2. Поиск в ширину
  3. Поиск в глубину
  4. Топологическая сортировка
  5. Компоненты сильной связности
  6. 2-SAT
  7. Мосты и точки сочленения
  8. Алгоритм Дейкстры
  9. A*
  10. Двунаправленный поиск
  11. Алгоритм Беллмана — Форда
  12. Алгоритм Флойда — Уоршелла
  13. Алгоритм Джонсона
  14. Иерархии сжатия
  15. Минимальное остовное дерево
  16. Максимальный поток
  17. Алгоритм Диница
  18. Минимальный разрез
  19. Раскраска графа
  20. Паросочетания в двудольном графе
  21. Венгерский алгоритм
  22. Поток наименьшей стоимости
  23. Эйлеров путь
  24. Наименьший общий предок
  25. Тяжёлые пути
  26. Центроидная декомпозиция
  27. Задача коммивояжёра
  28. PageRank

STR Строки и тексты

  1. Наивный поиск подстроки
  2. Префикс-функция и КМП
  3. Z-функция
  4. Полиномиальный хеш
  5. Алгоритм Рабина — Карпа
  6. Алгоритм Ахо — Корасик
  7. Бойер — Мур
  8. Суффиксный массив
  9. Суффиксный автомат
  10. Преобразование Барроуза — Уилера
  11. Алгоритм Манакера
  12. Редакционное расстояние
  13. BK-дерево
  14. Diff Майерса
  15. Код Хаффмана
  16. LZ77 и LZW

DP Динамическое программирование

  1. Динамическое программирование
  2. Задача о рюкзаке
  3. Наибольшая возрастающая подпоследовательность
  4. Наибольшая общая подпоследовательность
  5. Динамика на отрезках
  6. Динамика на деревьях
  7. Динамика по маскам
  8. Динамика по цифрам
  9. Convex hull trick
  10. Оптимизации Кнута и «разделяй и властвуй»
  11. Seam carving
  12. Алгоритм Витерби
  13. Выравнивание последовательностей
  14. Красивый перенос строк

PAR Парадигмы

  1. Жадные алгоритмы
  2. Перебор с возвратом
  3. Ветви и границы
  4. Разделяй и властвуй
  5. Ближайшая пара точек
  6. Встреча посередине

NUM Числа и алгебра

  1. Алгоритм Евклида
  2. Быстрое возведение в степень
  3. Решето Эратосфена
  4. Тест Миллера — Рабина
  5. ρ-алгоритм Полларда
  6. Китайская теорема об остатках
  7. Длинная арифметика
  8. Умножение Карацубы
  9. Быстрое преобразование Фурье
  10. Теоретико-числовое преобразование
  11. Матрицы и рекуррентности
  12. Метод Гаусса
  13. Генераторы псевдослучайных чисел

GEO Геометрия

  1. Векторы и ориентация
  2. Пересечение отрезков
  3. Выпуклая оболочка
  4. Точка в многоугольнике
  5. Площадь многоугольника
  6. Заметающая прямая
  7. Триангуляция Делоне и диаграмма Вороного
  8. k-d дерево
  9. R-дерево
  10. Вращающиеся калиперы
  11. Упрощение линий Рамера — Дугласа — Пекера
  12. Кривые Гильберта и geohash

NUM~ Численные методы

  1. Бисекция и метод Ньютона
  2. Градиентный спуск
  3. Численное интегрирование
  4. Метод Монте-Карло
  5. Методы Эйлера и Рунге — Кутты
  6. Суммирование Кэхэна
  7. Симплекс-метод

SYS Системы и большие данные

  1. Консистентное хеширование
  2. Рандеву-хеширование
  3. Count-Min Sketch
  4. HyperLogLog
  5. Резервуарная выборка
  6. Дерево Меркла
  7. Ограничение частоты запросов
  8. Инвертированный индекс и BM25
  9. MinHash и LSH
  10. LSM-дерево
  11. Повтор с джиттером
  12. Raft
  13. Часы Лэмпорта и векторные часы
  14. CRDT
  15. Два случайных выбора
  16. Планирование задач
  17. Алгоритм rsync
  18. LFU и ARC

ML Данные и машинное обучение

  1. k-средних
  2. DBSCAN
  3. k ближайших соседей
  4. Решающее дерево
  5. Наивный Байес
  6. Линейная регрессия
  7. Логистическая регрессия
  8. Метод главных компонент
  9. Рекомендации через разложение матрицы
  10. Перцептрон и обратное распространение

RND Случайность

  1. Лас-Вегас и Монте-Карло
  2. Случайные блуждания
  3. Имитация отжига
  4. Генетический алгоритм

TEC Приёмы

  1. O-большое
  2. Быстрый ввод и вывод
  3. Как честно измерить скорость
  4. Префиксные суммы
  5. Данные, удобные кэшу
  6. Два указателя
  7. Сколько операций в секунду
  8. Сложность по замерам
  9. Векторизация
  10. Скользящее окно
  11. Мемоизация
  12. Профилировщики
  13. Числа, которые стоит знать
  14. Амортизационный анализ
  15. Как выбрать структуру данных
  16. Сколько займёт в памяти
  17. Сжатие координат
  18. Битовые трюки
  19. Предподсчёт и таблицы
  20. Отсечения