|
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 | + |
| 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 | + |
| 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 | + |
| 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 | + |
| 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