Целочисленный двоичный поиск — различия между версиями
(→Применение двоичного поиска на неотсортированных массивах) |
м (Алгоритм про два возрастающих массива, записанных 1 за другим.) |
||
| Строка 74: | Строка 74: | ||
'''while''' l < r - 1 <font color="green">// Запускаем цикл...</font> | '''while''' l < r - 1 <font color="green">// Запускаем цикл...</font> | ||
m = (l + r) / 2 <font color="green">// m {{---}} середина области поиска.</font> | m = (l + r) / 2 <font color="green">// m {{---}} середина области поиска.</font> | ||
| − | '''if''' a[m] | + | '''if''' a[m] > a[n] <font color="green">// Сужение границ..</font> |
l = m | l = m | ||
'''else''' | '''else''' | ||
| Строка 89: | Строка 89: | ||
r = n + 1 | r = n + 1 | ||
</code> | </code> | ||
| − | Время выполнения данного алгоритма {{---}} <tex>O(2\log n)</tex>. | + | Время выполнения данного алгоритма {{---}} <tex>O(2\log n)=O(\log n)</tex>. |
==='''Применение поиска на массиве, отсортированном по возрастанию, в конец которого приписан массив, отсортированный по убыванию'''=== | ==='''Применение поиска на массиве, отсортированном по возрастанию, в конец которого приписан массив, отсортированный по убыванию'''=== | ||
| Строка 99: | Строка 99: | ||
'''while''' l < r - 1 <font color="green">// Запускаем цикл...</font> | '''while''' l < r - 1 <font color="green">// Запускаем цикл...</font> | ||
m = (l + r) / 2 <font color="green">// m {{---}} середина области поиска.</font> | m = (l + r) / 2 <font color="green">// m {{---}} середина области поиска.</font> | ||
| − | '''if''' a[m] > a[m - 1] <font color="green">// Сужение границ..</font> | + | '''if''' a[m] > a[m - 1] <font color="green">// Сужение границ...</font> |
l = m | l = m | ||
'''else''' | '''else''' | ||
| Строка 107: | Строка 107: | ||
Затем запустим левосторонний двоичный поиск для каждого массива отдельно: для элементов <tex>[0..x]</tex> и для элементов <tex>[x+1..n]</tex>. Для массива, отсортированного по убыванию используем двоичный поиск, измененнив условие в <tex>if</tex> на <tex>a[m] > key</tex>. | Затем запустим левосторонний двоичный поиск для каждого массива отдельно: для элементов <tex>[0..x]</tex> и для элементов <tex>[x+1..n]</tex>. Для массива, отсортированного по убыванию используем двоичный поиск, измененнив условие в <tex>if</tex> на <tex>a[m] > key</tex>. | ||
| − | Время выполнения алгоритма {{---}} <tex>O(3\log n)</tex>. | + | Время выполнения алгоритма {{---}} <tex>O(3\log n)=O(\log n)</tex>. |
==='''Применение поиска на двух отсортированных по возрастанию массивах, записанных один в конец другого'''=== | ==='''Применение поиска на двух отсортированных по возрастанию массивах, записанных один в конец другого'''=== | ||
| − | + | Найдем индекс последнего элемента левого массива, заменив условие в <tex>if</tex> на <tex>a[m] < a[m-1]</tex>. Так мы найдем единственный элемент в массиве, который меньше предыдущего элемента (все остальные элементы больше предыдущего, так как массивы отсортированы по возрастанию). Однако такой алгоритм не будет работать, если левый массив длиннее, чем правый, и <tex>last</tex> будет равняться начальному значению <tex>l</tex>. Поэтому будем запускать такой алгоритм рекурсивно, если <tex>last</tex> равно начальному значению <tex>l</tex>, беря за новое начальное значение <tex>l</tex> середину массива. Таким образом мы запустим бинарный поиск еще <tex>O(\log n)</tex> раз, постоянно уменьшая массив в два раза. Тогда через <tex>O(\log n)</tex> итераций либо левый массив перестанет быть длиннее, чем правый, и значение <tex>last</tex> окажется отличным от <tex>a</tex> (начальное значение <tex>l</tex>), либо длина массива станет равной двум и рекурсия потеряет всякий смысл (если в таком случае <tex>last</tex> останется равным <tex>a</tex>, то, значит, что первый элемент правого массива больше последнего элемента левого массива. Тогда два массива будут являться одним массивом отсортированным по возрастанию). После выполнения этого алгоритма <tex>last</tex> и будет номером последнего элемента из левого массива. | |
| − | Время выполнения алгоритма {{---}} <tex>O( | + | <code> |
| + | <font color="green">// Поиск последнего элемента левого массива</font> | ||
| + | '''function''' indexOfLastLeftArrayElement('''int''' a,'''int''' b) | ||
| + | '''int''' l = a | ||
| + | '''int''' r = b | ||
| + | '''while''' l < r - 1 <font color="green">// Запускаем цикл...</font> | ||
| + | m = (l + r) / 2 <font color="green">// m {{---}} середина области поиска.</font> | ||
| + | '''if''' a[m] < a[m - 1] <font color="green">// Сравнение с предыдущим...</font> | ||
| + | l = m | ||
| + | '''else''' | ||
| + | r = m | ||
| + | '''int''' last = l | ||
| + | '''if''' last == a '''and''' r - (a + b) / 2 > 1 | ||
| + | indexOfLastLeftArrayElement((a + b) / 2, r) | ||
| + | </code> | ||
| + | |||
| + | Проверим, равен ли <tex>last</tex> <tex>a</tex>. Если да, то запустим бинарный поиск на массиве от <tex>0</tex> до <tex>n</tex>. Иначе, зная номер последнего элемента левого массива, запустим два раза левосторонний бинарный поиск: на массиве от <tex>0</tex> до <tex>last</tex>, и на массиве от <tex>last + 1</tex> до <tex>n</tex>. | ||
| + | |||
| + | Время выполнения алгоритма {{---}} <tex>O(\log^2 n)</tex> (мы запускаем бинарный поиск, который требует <tex>O(\log n)</tex> времени <tex>O(\log n)</tex> раз. | ||
==='''Применение поиска на циклически сдвинутом массиве, образованном приписыванием отсортированного по убыванию массива в конец отсортированного по возрастанию'''=== | ==='''Применение поиска на циклически сдвинутом массиве, образованном приписыванием отсортированного по убыванию массива в конец отсортированного по возрастанию'''=== | ||
Версия 23:05, 6 декабря 2015
Целочисленный двоичный поиск (бинарный поиск) (англ. 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
В случае правостороннего поиска изменится знак сравнения при сужении границ на .
Инвариант цикла: пусть левый индекс не больше искомого элемента, а правый — строго больше, тогда если , то понятно, что — самое правое вхождение (так как следующее уже больше).
Несколько слов об эвристиках
Эвристика с завершением поиска, при досрочном нахождении искомого элемента
Заметим, что если нам необходимо просто проверить наличие элемента в упорядоченном множестве, то можно использовать любой из правостороннего и левостороннего поиска. При этом будем на каждой итерации проверять "не попали ли мы в элемент, равный искомому", и в случае попадания заканчивать поиск.
Эвристика с запоминанием ответа на предыдущий запрос
Пусть дан отсортированный массив чисел, упорядоченных по неубыванию. Также пусть запросы приходят в таком порядке, что каждый следующий не меньше, чем предыдущий. Для ответа на запрос будем использовать левосторонний двоичный поиск. При этом после того как обработался первый запрос, запомним чему равно , запишем его в переменную . Когда будем обрабатывать следующий запрос, то проинициализируем левую границу как . Заметим, что все элементы, которые лежат не правее , строго меньше текущего искомого элемента, так как они меньше предыдущего запроса, а значит и меньше текущего. Значит инвариант цикла выполнен.
Применение двоичного поиска на неотсортированных массивах
Применение поиска на циклически сдвинутом отсортированном массиве
Пусть отсортированный по возрастанию массив , все элементы которого различны, был циклически сдвинут, тогда полученный массив состоит из двух отсортированных частей. Используем двоичный поиск, чтобы найти индекс последнего элемента левой части массива. Для этого в реализации двоичного поиска заменим условие в на , тогда в будет содержаться искомый индекс:
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