Skip to content

Commit 13829a8

Browse files
agorinenkoAnton Gorinenko
andauthored
Бинарный поиск
Co-authored-by: Anton Gorinenko <[email protected]>
1 parent 9d48f5e commit 13829a8

File tree

5 files changed

+224
-1
lines changed

5 files changed

+224
-1
lines changed

img/binary_search.jpg

24.3 KB
Loading

img/binary_search_1.png

27.9 KB
Loading

img/binary_search_2.png

28 KB
Loading

img/binary_search_3.png

25.5 KB
Loading

tutorial/binary_search.md

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,224 @@
1-
# Бинарный поиск
1+
# Бинарный поиск
2+
3+
Бинарный поиск — процесс нахождения индекса элемента с целевым значением в **отсортированном** массиве путем его
4+
дробления на половину на каждом шаге новой итерации. Изначально алгоритм поиска сравнивает искомое значение со средним
5+
элементом в массиве. Если значения не равны, то он отбрасывает ту часть массива, в которой целевое значение
6+
гарантированно не может находиться. Далее поиск продолжается в оставшейся части элементов путем сравнения средних
7+
элементов с искомым значением до тех пор, пока оно не будет найдено, либо пока оставшаяся часть не станет пустой. В этом
8+
случае мы можем сказать, что элемент не найден.
9+
10+
Бинарный поиск имеет логарифмическую временную сложность **O(logN)** и постоянную пространственную сложность по
11+
памяти **O(1)**. На больших массивах бинарный поиск работает быстрее линейного поиска, однако список изначально должен
12+
быть отсортирован.
13+
14+
Терминология, используемая в бинарном поиске:
15+
16+
1. target - значение, которое требуется найти
17+
2. index - индекс значения, которое требуется найти
18+
3. left, right - левый и правый индексы, которые определяют область поиска
19+
4. mid - индекс среднего элемента в текущей области поиска, который мы будем сравнивать с целевым значением
20+
21+
Наглядное представление алгоритма поиска следующее:
22+
23+
![Бинарный поиск](../img/binary_search.jpg)
24+
25+
Самую простую и понятную реализацию поиска числа в массиве можно представить так:
26+
27+
```python
28+
def binary_search(arr: list[int], target: int) -> int:
29+
"""
30+
Двоичный (бинарный) поиск (дихотомия)
31+
:param arr: массив для поиска.
32+
:param target: элемент, который нужно найти.
33+
:return: индекс элемента или -1 если не найдено
34+
"""
35+
left = 0
36+
right = len(arr) - 1
37+
while left <= right:
38+
mid = (left + right) // 2
39+
middle = arr[mid]
40+
if middle < target:
41+
left = mid + 1
42+
elif middle > target:
43+
right = mid - 1
44+
else:
45+
return mid
46+
return -1
47+
```
48+
49+
Здесь мы постепенно сужаем область поиска, используя условие сравнения ``middle`` с ``target``. После цикла while нет
50+
дополнительной постобработки, мы просто возвращаем -1, элемент не найден.
51+
52+
**Продвинутый бинарный поиск**
53+
54+
Иногда полезны другие вариации бинарного поиска, где используется условие сравнения ``middle`` с его соседями. Например
55+
задача нахождения любого локального максимума в последовательности. Локальный максимум — это элемент, который всегда
56+
больше своих соседей. Следовательно, если ``middle`` меньше либо равен, чем следующий элемент, мы двигаем левую границу,
57+
в противном случае правую.
58+
59+
Допустим нам нужно найти локальный максимум в последовательности ``[1,2,1,3,5,6,4]``. Это числа 2 и 6. Наглядно это
60+
выглядит так.
61+
62+
![Локальный максимум](../img/binary_search_3.png)
63+
64+
На начальном шаге число 3 сравнивается с 5. Двигаем левую границу к 3, т.к 3 меньше 5. Далее сравниваем 6 и 4. 6 больше
65+
— двигаем правую к 6. На последнем шаге сравниваем 5 и 6. 5 меньше — устанавливаем левую границу на 6 и выходим из
66+
цикла, т.к. правая и левая границы совпадают. Возвращаем левую границу, т.к. в последовательности всегда найдется
67+
максимум. Пример кода следующий:
68+
69+
```python
70+
def find_max_element(nums: list[int]) -> int:
71+
"""
72+
Нахождение локального максимума
73+
:param nums: последовательность чисел
74+
:return: индекс максимума
75+
"""
76+
if len(nums) == 1:
77+
return 0
78+
79+
left, right = 0, len(nums) - 1
80+
81+
if len(nums) == 2 and nums[left] < nums[right]:
82+
return right
83+
84+
while left < right:
85+
mid = (left + right) // 2
86+
if nums[mid] > nums[mid + 1]:
87+
right = mid
88+
else:
89+
left = mid + 1
90+
91+
return left
92+
```
93+
94+
**Левый бинарный поиск** — это задача нахождения первого подходящего значения на интервале, где функция сначала
95+
принимает значение ``0``, а затем ``1``.
96+
97+
![Левый бинарный поиск](../img/binary_search_1.png)
98+
99+
Ниже представлена реализация левого бинарного поиска:
100+
101+
```python
102+
def left_binary_search(left: int, right: int, check, *args) -> int:
103+
"""
104+
Левый бинарный поиск. Задача нахождения первого подходящего значения
105+
___плохо___|---хорошо---
106+
:param left: указатель на минимальное значение функции
107+
:param right: указатель на максимальное значение функции
108+
:param check: функция проверки условия
109+
:param args: аргументы функции проверки условия
110+
:return: индекс первого элемента, удовлетворяющего условию
111+
"""
112+
while left < right:
113+
mid = (left + right) // 2
114+
if check(mid, *args):
115+
right = mid
116+
else:
117+
left = mid + 1
118+
119+
return left
120+
```
121+
122+
Рассмотрим пример поиска числа 6 ниже. Заметим, что
123+
124+
1. Функция бинарного поиска возвращает индекс найденного элемента
125+
1. В случае, если мы ищем число, выходящее за правую границу области поиска, результатом работы будет индекс самого
126+
правого элемента. Например в задаче нахождения числа 13, вернется индекс числа 10
127+
1. В случае, если мы ищем число, выходящее за левую границу области поиска, результатом работы будет индекс самого
128+
левого элемента. Например в задаче нахождения числа -2, вернется индекс числа 0
129+
130+
```python
131+
def test_left_binary_search():
132+
# Определяем интервал поиска
133+
arr = [0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
134+
# Определяем границы нашей функции
135+
left = 0
136+
right = len(arr) - 1
137+
# Искомое число
138+
target = 6
139+
140+
def find_target(mid):
141+
return arr[mid] >= target
142+
143+
assert 9 == left_binary_search(left, right, find_target)
144+
```
145+
146+
**Правый бинарный поиск** — это задача нахождения последнего подходящего значения на интервале, где функция сначала
147+
принимает значение ``1``, а затем ``0``.
148+
149+
![Правый бинарный поиск](../img/binary_search_2.png)
150+
151+
Ниже представлена реализация правого бинарного поиска:
152+
153+
```python
154+
def right_binary_search(left: int, right: int, check, *args) -> int:
155+
"""
156+
Правый бинарный поиск. Задача нахождения последнего подходящего значения
157+
---хорошо---|___плохо___
158+
:param left: указатель на минимальное значение функции
159+
:param right: указатель на максимальное значение функции
160+
:param check: функция проверки условия
161+
:param args: аргументы функции проверки условия
162+
:return: индекс последнего элемента, удовлетворяющего условию
163+
"""
164+
while left < right:
165+
mid = (left + right + 1) // 2
166+
if check(mid, *args):
167+
left = mid
168+
else:
169+
right = mid - 1
170+
171+
return left
172+
```
173+
174+
Используя правый бинарный поиск в задаче нахождения числа 6, следует лишь поменять условие в функции проверки:
175+
176+
```python
177+
def test_right_binary_search():
178+
# Определяем интервал поиска
179+
arr = [0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
180+
# Определяем границы нашей функции
181+
left = 0
182+
right = len(arr) - 1
183+
# Искомое число
184+
target = 6
185+
186+
def find_target(mid):
187+
return arr[mid] <= target
188+
189+
assert 9 == right_binary_search(left, right, find_target)
190+
```
191+
192+
Используя комбинацию левого и правого поисков можно решить задачу нахождения интервала в отсортированной неубывающей
193+
последовательности. Например, так:
194+
195+
```python
196+
def search_range(nums: List[int], target: int) -> List[int]:
197+
if len(nums) == 0:
198+
return [-1, -1]
199+
200+
left, right = 0, len(nums) - 1
201+
202+
def find_left_target(mid):
203+
return nums[mid] >= target
204+
205+
def find_right_target(mid):
206+
return nums[mid] <= target
207+
208+
left_idx = left_binary_search(left, right, find_left_target)
209+
210+
if nums[left_idx] != target:
211+
left_idx = -1
212+
213+
right_idx = right_binary_search(left, right, find_right_target)
214+
215+
if nums[right_idx] != target:
216+
right_idx = -1
217+
218+
return [left_idx, right_idx]
219+
220+
221+
def test_search_range():
222+
nums = [5, 7, 7, 8, 8, 10]
223+
assert search_range(nums, 8) == [3, 4]
224+
```

0 commit comments

Comments
 (0)