Целочисленный двоичный поиск
Целочисленный двоичный поиск (бинарный поиск) (англ. binary search) — алгоритм поиска объекта по заданному признаку в множестве объектов, упорядоченных по тому же самому признаку, работающий за логарифмическое время.
Содержание
- 1 Формулировка задачи
- 2 Принцип работы
- 3 Правосторонний/левосторонний целочисленный двоичный поиск
- 4 Алгоритм двоичного поиска
- 5 Код
- 6 Несколько слов об эвристиках
- 7 Применение двоичного поиска на некоторых неотсортированных массивах
- 7.1 Применение поиска на циклически сдвинутом отсортированном массиве
- 7.2 Применение поиска на массиве, отсортированном по возрастанию, в конец которого приписан массив, отсортированный по убыванию
- 7.3 Применение поиска на двух отсортированных по возрастанию массивах, записанных один в конец другого
- 7.4 Применение поиска на циклически сдвинутом массиве, образованном приписыванием отсортированного по убыванию массива в конец отсортированного по возрастанию
- 8 См. также
- 9 Источники информации
Формулировка задачи
Пусть нам дан упорядоченный массив, состоящий только из целочисленных элементов. Требуется найти позицию, на которой находится заданный элемент. Для этой задачи мы и можем использовать двоичный поиск.
Принцип работы
Двоичный поиск заключается в том, что на каждом шаге множество объектов делится на две части и в работе остаётся та часть множества, где находится искомый объект. Или же, в зависимости от постановки задачи, мы можем остановить процесс, когда мы получим первый или же последний индекс вхождения элемента. Последнее условие — это левосторонний/правосторонний двоичный поиск.
Правосторонний/левосторонний целочисленный двоичный поиск
Для простоты дальнейших определений будем считать, что
и что .
Определение: |
Правосторонний бинарный поиск (англ. 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
В случае правостороннего поиска изменится знак сравнения при сужении границ на .
Инвариант цикла: пусть левый индекс не больше искомого элемента, а правый — строго больше, тогда если
, то понятно, что — самое правое вхождение (так как следующее уже больше).Несколько слов об эвристиках
Эвристика с завершением поиска, при досрочном нахождении искомого элемента
Заметим, что если нам необходимо просто проверить наличие элемента в упорядоченном множестве, то можно использовать любой из правостороннего и левостороннего поиска. При этом будем на каждой итерации проверять "не попали ли мы в элемент, равный искомому", и в случае попадания заканчивать поиск.
Эвристика с запоминанием ответа на предыдущий запрос
Пусть дан отсортированный массив чисел, упорядоченных по неубыванию. Также пусть запросы приходят в таком порядке, что каждый следующий не меньше, чем предыдущий. Для ответа на запрос будем использовать левосторонний двоичный поиск. При этом после того как обработался первый запрос, запомним чему равно
, запишем его в переменную . Когда будем обрабатывать следующий запрос, то проинициализируем левую границу как . Заметим, что все элементы, которые лежат не правее , строго меньше текущего искомого элемента, так как они меньше предыдущего запроса, а значит и меньше текущего. Значит инвариант цикла выполнен.Применение двоичного поиска на некоторых неотсортированных массивах
Применение поиска на циклически сдвинутом отсортированном массиве
Пусть отсортированный по возрастанию массив из if
на , тогда в будет содержаться искомый индекс:
int l = 0 int r = n + 1 while l < r - 1 // Запускаем цикл m = (l + r) / 2 // m — середина области поиска if a[m] > a[n - 1] // Сужение границ 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
Время выполнения данного алгоритма —
.Применение поиска на массиве, отсортированном по возрастанию, в конец которого приписан массив, отсортированный по убыванию
Найдем индекс последнего элемента массива, отсортированного по возрастанию, воспользовавшись двоичным поиском, условие в if
изменим на . Тогда в будет содержаться искомый индекс:
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 — искомый индекс.
Затем запустим левосторонний двоичный поиск для каждого массива отдельно: для элементов if
на .
Время выполнения алгоритма —
.
Применение поиска на двух отсортированных по возрастанию массивах, записанных один в конец другого
Мы имеем массив, образованный из двух отсортированных массивов, записанных один в конец другого. Рассмотрим разные примеры таких массивов (вертикальная черта означает границу между двумя маленькими массивами, образующими большой, никакой смысловой нагрузки она не несет, она нужна лишь для облегчения понимания):
- или — самые частые варианты.
- — оба массива имеют длину .
- — все элементы из второго массива больше последнего (максимального) элемента из первого массива. Стоит заметить, что массив в этом примере не отличим от массивов: , и тому подобных.
Запустить сразу бинарный поиск на таком массиве (образованном из двух маленьких) не представляется разумным, так как массив не будет обязательно отсортированным. Также нельзя запустить другие поиски, работающие за
, так как точек экстремума несколько, и нет никакой дополнительной информации об элементах в массивах. Поэтому попробуем найти индекс последнего элемента левого массива, чтобы потом запустить бинарный поиск два раза на отсортированных массивах. Найти последний элемент левого массива с помощью алгоритмов поиска, которые ищут максимум на массиве тоже невозможно, потому что элементы правого массива (все или некоторые) могут быть больше последнего элемента левого массива.Рассмотрим массивы
и . Все элементы, кроме пятого не меняются, значит, по другим элементам невозможно определить, есть ли в правом массиве элемент, который меньше элементов левого массива. Поэтому для нахождения конца левого массива придется сравнить все элементы с соседними за , тогда проще сразу искать нужный элемент, а не конец левого массива.Для того, чтобы за
найти элемент в массиве пройти по всем элементам массива и сравнить их с искомым.
Применение поиска на циклически сдвинутом массиве, образованном приписыванием отсортированного по убыванию массива в конец отсортированного по возрастанию
После циклического сдвига мы получим массив if
на (ответ будет записан в ) или на (ответ будет записан в ) соответственно:
// Поиск максимума 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