update russian v2

This commit is contained in:
krahets
2026-03-25 16:54:42 +08:00
parent 036b41b5cb
commit d2da1b4a83
20 changed files with 25011 additions and 0 deletions
+543
View File
@@ -0,0 +1,543 @@
# Поиск
![](ru/docs/assets/media/image511.jpeg){width="3.5416633858267716in" height="4.583333333333333in"}
#### двоичный поиск
> Двоичный (бинарный) поиск -- это эффективный алгоритм поиска, основан- ный на стратегии «разделяй и властвуй». Он использует упорядоченность дан- ных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или область поиска не станет пустой.
![](ru/docs/assets/media/image513.jpeg)
> **Рис. 10.1.** Пример данных для двоичного поиска
>
> Как показано на рис. 10.2, сначала инициализируются указатели *i* = 0 и *j* = *n* − 1, которые указывают на первый и последний элементы массива и представляют область поиска \[0, *n* − 1\]. Обратите внимание, что квадратные скобки обозначают замкнутый интервал, включающий граничные значения.
>
> Затем в цикле выполняются следующие два шага:
1) вычисляется индекс средней точки *m* = ⌊(*i* + *j*)/2⌋, где ⌊ ⌋ обозначает опера- цию округления вниз;
2) определяется соотношение между nums\[m\] и target, выделяются три случая:
- если nums\[m\] \< target, то target находится в интервале \[*m* + 1, *j*\], поэтому выполняется *i* = *m* + 1;
- если nums\[m\] \> target, то target находится в интервале \[*i*, *m* − 1\], поэтому выполняется *j* = *m* 1;
- если nums\[m\] = target, то target найден, и возвращается индекс *m*.
> Если массив не содержит целевой элемент, область поиска в конечном итоге сократится до пустой. В этом случае возвращается −1.
>
> ![](ru/docs/assets/media/image515.jpeg)
![](ru/docs/assets/media/image517.jpeg)![](ru/docs/assets/media/image519.jpeg)
> **Рис. 10.2.** Процесс двоичного поиска. Шаги 1--3
![](ru/docs/assets/media/image521.jpeg)
> ![](ru/docs/assets/media/image523.jpeg)
![](ru/docs/assets/media/image525.jpeg)
> **Рис. 10.2.** *Продолжение*. Шаги 4--6
>
> ![](ru/docs/assets/media/image527.jpeg)
>
> **Рис. 10.2.** *Окончание*. Шаг 7
>
> Следует отметить, что, поскольку *i* и *j* имеют тип int, **сумма** *i* + *j* **может превысить допустимый диапазон значений типа** int. Чтобы избежать переполнения, обычно для вычисления средней точки используется формула *m* = ⌊*i* + (*j* *i*)/2⌋.
>
> Ниже приведен пример кода.
>
> \# === File: binary_search.py ===
>
> def binary_search(nums: list\[int\], target: int) -\> int: \"\"\" Двоичный поиск (двойной замкнутый интервал).\"\"\"
>
> \# Инициализация двойного замкнутого интервала \[0, n-1\], i и j указывают
>
> \# на первый и последний элементы массива. i, j = 0, len(nums) - 1
>
> \# Цикл, выход при пустом интервале поиска (когда i \> j). while i \<= j:
>
> \# Теоретически числа в Python могут быть бесконечно большими (зависит
>
> \# от объема памяти), и нет необходимости учитывать переполнение. m = (i + j) // 2 \# Вычисление индекса средней точки m.
>
> if nums\[m\] \< target:
>
> i = m + 1 \# В этом случае target находится в интервале \[m+1, j\]. elif nums\[m\] \> target:
>
> j = m - 1 \# В этом случае target находится в интервале \[i, m-1\].
>
> else:
>
> return m \# Найден целевой элемент, возвращается его индекс. return -1 \# Целевой элемент не найден, возвращается -1.
>
> **Временная сложность** составляет *O*(log *n*): в цикле двоичного поиска об- ласть поиска сокращается вдвое на каждом шаге, поэтому количество итера- ций равно log2 *n*.
>
> **Пространственная сложность** составляет *O*(1): указатели *i* и *j* занимают
>
> постоянное количество памяти.
1. **Методы представления интервалов**
> Кроме указанного выше двойного замкнутого интервала, существует также ле- возамкнутый правооткрытый интервал \[0, *n*), т. е. левая граница включается, а правая -- нет. В этом представлении интервал \[*i*, *j*) пуст, когда *i* = *j*.
>
> На основе этого представления можно реализовать двоичный поиск с ана- логичной функциональностью.
>
> \# === File: binary_search.py ===
>
> def binary_search_lcro(nums: list\[int\], target: int) -\> int:
>
> \"\"\" Двоичный поиск (левозамкнутый правооткрытый интервал).\"\"\"
>
> \# Инициализация левозамкнутого правооткрытого интервала \[0, n), i и j ука- зывают на первый элемент массива и элемент после последнего.
>
> i, j = 0, len(nums)
>
> \# Цикл, выход при пустом интервале поиска (когда i = j). while i \< j:
>
> m = (i + j) // 2 \# Вычисление индекса средней точки m. if nums\[m\] \< target:
>
> i = m + 1 \# В этом случае target находится в интервале \[m+1, j). elif nums\[m\] \> target:
>
> j = m \# В этом случае target находится в интервале \[i, m).
>
> else:
>
> return m \# Найден целевой элемент, возвращается его индекс. return -1 \# Целевой элемент не найден, возвращается -1.
>
> В двух представлениях интервалов инициализация, условия цикла и опера- ции сокращения интервала в алгоритме двоичного поиска различаются, как показано на рис. 10.3.
>
> Поскольку в представлении «двойной замкнутый интервал» обе границы определены как замкнутые, операции сокращения интервала с помощью указа- телей *i* и *j* также симметричны. Это снижает вероятность ошибок, поэтому обыч- но **рекомендуется использовать запись «двойной замкнутый интервал»**.
>
> ![](ru/docs/assets/media/image529.jpeg)**Элемент**
>
> Индекс
>
> Обе границы включены в интервал
>
> Интервал поиска: двойной замкнутый \[**i**, **j**\] Инициализация указателей: **i** = 0, **j** = n − 1 Условие завершения цикла: **i** \> **j**
>
> Сужение интервала: **i** = **m** + 1, **j** = **m** 1
>
> Интервал поиска: левозамкнутый правооот- крытый \[**i**, **j**)
>
> Инициализация указателей: **i** = 0, **j** = n Условие завершения цикла: **i** ≥ **j**
>
> Операция сужения интервала: **i** = **m** + 1, **j** = **m**
>
> **Рис. 10.3.** Два определения интервалов
### Преимущества и ограничения
> Двоичный поиск обладает хорошей производительностью как по времени, так и по пространству.
- Двоичный поиск отличается высокой эффективностью по времени. При большом объеме данных логарифмическая временная сложность име- ет значительное преимущество. Например, при размере данных *n* = 220 линейный поиск требует 220 = 1 048 576 итераций, тогда как двоичный поиск -- всего log2 220 = 20 итераций.
- Двоичный поиск, в отличие от некоторых других алгоритмов поиска (на-
> пример, хеш-поиска), не требует дополнительного пространства и по- этому более экономичен в плане использования памяти.
>
> Тем не менее двоичный поиск не подходит для всех случаев по следующим основным причинам.
- Двоичный поиск применим только к упорядоченным данным. Если входные данные неупорядоченные, то их сортировка специально для использования двоичного поиска не оправдана. Это связано с тем, что временная сложность алгоритмов сортировки обычно составляет *O*(*n* log *n*), что выше, чем у линейного и двоичного поиска. В сценари- ях с частыми добавлениями элементов для поддержания упорядочен- ности массива необходимо вставлять элементы в определенные по- зиции, что имеет временную сложность *O*(*n*) и также является весьма затратным.
- Двоичный поиск применим только к массивам, поскольку требует скач- кообразного (непрерывного) доступа к элементам. В связных списках выполнение скачкообразного доступа менее эффективно, поэтому такой поиск не подходит для применения в связных списках и структурах дан- ных, основанных на них.
- При небольших объемах данных линейный поиск более эффективен. В линейном поиске на каждом этапе требуется только одна операция сравнения; в двоичном поиске требуется одна операция сложения, одна операция деления, от одной до трех операций сравнения и одна опера- ция сложения (вычитания), всего от четырех до шести элементарных операций. Поэтому, когда объем данных *n* невелик, линейный поиск ока- зывается быстрее двоичного.
#### вставка с использованием двоичного поиска
> Двоичный поиск можно использовать не только для поиска целевого элемен- та, но и для решения множества других задач, таких как поиск позиции для вставки целевого элемента.
### Случай без повторяющихся элементов
![](ru/docs/assets/media/image531.jpeg)
> **Рис. 10.4.** Пример данных для вставки с использованием двоичного поиска
>
> Если требуется повторно использовать код двоичного поиска из предыду- щего раздела, необходимо ответить на следующие два вопроса:
1) если массив содержит target, является ли индекс вставки индексом этого элемента? Условие задачи требует вставить target слева от равного эле- мента, т. е. новый target заменяет старое положение target. То есть **если массив уже содержит** target, **индекс вставки совпадает с индексом этого** target;
2) если массив не содержит target, какой элемент будет иметь индекс вставки?
> Дальнейший процесс двоичного поиска: когда nums\[m\] \< target, указатель *i* перемещается, т. е. приближается к элементу, большему или равному target. Аналогично указатель *j* всегда приближается к элементу, меньшему или рав- ному target.
>
> Таким образом, по окончании двоичного поиска указатель *i* указывает на первый элемент, больший target, а указатель *j* -- на первый элемент, меньший target. **Легко понять**, **что**, **если массив не содержит** target, **индекс вставки будет равен** *i*. Ниже приведен пример кода.
>
> \# === File: binary_search_insertion.py ===
>
> def binary_search_insertion_simple(nums: list\[int\], target: int) -\> int: \"\"\" Двоичный поиск точки вставки (без повторяющихся элементов).\"\"\"
>
> i, j = 0, len(nums) - 1 \# Инициализация двойного закрытого интервала \[0, n-1\]. while i \<= j:
>
> m = (i + j) // 2 \# Вычисление среднего индекса m. if nums\[m\] \< target:
>
> i = m + 1 \# target находится в интервале \[m+1, j\]. elif nums\[m\] \> target:
>
> j = m - 1 \# target находится в интервале \[i, m-1\].
>
> else:
>
> return m \# Найден target, возвращается точка вставки m.
>
> \# target не найден, возвращается точка вставки i. return i
### Случай с повторяющимися элементами
> Если в массиве существует несколько одинаковых target, то обычный двоич- ный поиск может вернуть индекс только одного из них, не определяя, сколько target находится слева и справа от этого элемента.
>
> Задача требует вставить целевой элемент в самое левое положение, поэто- му необходимо найти индекс самого левого target в массиве. Первоначально предполагается реализовать решение следующим образом (см. рис. 10.5):
1) выполнить двоичный поиск и получить индекс любого target, обозна- чить его как *k*;
2) начиная с индекса *k*, выполнить линейный обход влево и вернуть ин- декс, когда будет найден самый левый target.
![](ru/docs/assets/media/image533.jpeg)
> **Рис. 10.5.** Линейный поиск точки вставки для повторяющихся элементов
>
> Это рабочий метод, но он включает линейный поиск, поэтому его времен- ная сложность составляет *O*(*n*). Когда в массиве много повторяющихся target, эффективность этого метода низка.
>
> Теперь рассмотрим расширение алгоритма двоичного поиска. Общий про- цесс остается неизменным: на каждом этапе сначала вычисляется средний ин- декс *m*, затем определяется отношение между target и nums\[m\], как показано на рис. 10.6. Возможны два случая:
1) nums\[m\] \< target или nums\[m\] \> target, тогда target еще не найден, поэтому используется операция сужения интервала обычного двоичного поиска, **чтобы указатели** *i* **и** *j* **приближались к** target;
2) nums\[m\] == target, тогда элементы, меньшие target, находятся в интервале \[*i*, *m* -- 1\]. Поэтому используется операция *j* = *m* -- 1 для сужения интерва- ла, **чтобы указатель** *j* **приблизился к элементам**, **меньшим** target.
> После завершения цикла *i* будет указывать на самый левый target, а *j* -- на первый элемент, меньший target. **Поэтому индекс** *i* **является точкой вставки**.
![](ru/docs/assets/media/image535.jpeg)![](ru/docs/assets/media/image537.jpeg)
> **Рис. 10.6.** Этапы двоичного поиска точки вставки для повторяющихся элементов. Шаги 1--2
![](ru/docs/assets/media/image523.jpeg)
> ![](ru/docs/assets/media/image539.jpeg)
![](ru/docs/assets/media/image541.jpeg)
> **Рис. 10.6.** *Продолжение*. Шаги 3--5
>
> ![](ru/docs/assets/media/image543.jpeg)
![](ru/docs/assets/media/image545.jpeg)![](ru/docs/assets/media/image547.jpeg)
> **Рис. 10.6.** *Окончание*. Шаги 6--8
>
> Ниже приведен пример кода. Операции в ветвях nums\[m\] \> target и nums\[m\] == target одинаковы, поэтому их можно объединить. Тем не менее условие можно оставить развернутым, так как это делает логику более ясной и улучшает чи- таемость.
>
> \# === File: binary_search_insertion.py ===
>
> def binary_search_insertion(nums: list\[int\], target: int) -\> int:
>
> \"\"\" Двоичный поиск точки вставки (с повторяющимися элементами).\"\"\"
>
> i, j = 0, len(nums) - 1 \# Инициализация двойного закрытого интервала \[0, n-1\].
>
> while i \<= j:
>
> m = (i + j) // 2 \# Вычисление индекса середины m. if nums\[m\] \< target:
>
> i = m + 1 \# target в интервале \[m+1, j\]. elif nums\[m\] \> target:
>
> j = m - 1 \# target в интервале \[i, m-1\].
>
> else:
>
> j = m - 1 \# Первый элемент, меньший target, в интервале \[i, m-1\]. \# Возврат точки вставки i.
>
> return i
>
> Подводя итоги, можно сказать, что двоичный поиск заключается в установке целей поиска для указателей *i* и *j*. Целью может быть конкретный элемент (на- пример, target) или диапазон элементов (например, элементы, меньшие target). В процессе повторяющегося двоичного поиска указатели *i* и *j* постепенно приближаются к заранее установленной цели. В конечном итоге они либо
>
> успешно находят ответ, либо останавливаются после выхода за границы.
#### двоичный поиск границ
### Поиск левой границы
3. Двоичный поиск границ ❖ **277**
> Вспомним метод двоичного поиска точки вставки: после завершения поис- ка индекс *i* указывает на самый левый элемент target, **поэтому поиск точки вставки, по сути, является поиском индекса самого левого** target.
>
> Рассмотрим реализацию поиска левой границы через функцию поиска точ- ки вставки. Обратите внимание, что массив может не содержать target, что мо- жет привести к следующим двум результатам:
1) индекс точки вставки *i* выходит за границы;
2) элемент nums\[i\] не равен target.
> При возникновении этих двух ситуаций следует сразу вернуть --1. Код реа- лизации приведен ниже.
>
> \# === File: binary_search_edge.py ===
>
> def binary_search_left_edge(nums: list\[int\], target: int) -\> int: \"\"\" Двоичный поиск самого левого элемента target.\"\"\"
>
> \# Эквивалентно поиску точки вставки target. i = binary_search_insertion(nums, target)
>
> \# target не найден, возвращается -1.
>
> if i == len(nums) or nums\[i\] != target: return -1
>
> \# target найден, возвращается индекс i. return i
### Поиск правой границы
> Как найти самый правый элемент target? Самый очевидный способ -- изменить код, заменив операцию сужения указателя в случае nums\[m\] == target. Мы не будем приводить код для этого случая, заинтересованные читатели могут реа- лизовать его самостоятельно.
>
> Ниже представлены два более изящных подхода.
##### Повторное использование поиска левой границы
> На самом деле можно использовать функцию поиска самого левого элемента для поиска самого правого элемента. Что именно нужно сделать: преобразо- вать поиск самого правого элемента target в поиск самого левого target + 1.
>
> После завершения поиска указатель *i* указывает на самый левый элемент target + 1 (если он существует), а *j* указывает на самый правый target, поэтому можно вернуть *j*, как показано на рис. 10.7.
>
> ![](ru/docs/assets/media/image549.jpeg)
>
> **Рис. 10.7.** Преобразование поиска правой границы в поиск левой границы
>
> Обратите внимание, что возвращаемая точка вставки -- это *i*, поэтому необ- ходимо вычесть 1, чтобы получить *j*.
>
> \# === File: binary_search_edge.py ===
>
> def binary_search_right_edge(nums: list\[int\], target: int) -\> int: \"\"\" Двоичный поиск самого правого target.\"\"\"
>
> \# Преобразование в поиск самого левого target + 1. i = binary_search_insertion(nums, target + 1)
>
> \# j указывает на самый правый target, i указывает на первый элемент,
>
> \# больший target. j = i - 1
>
> \# target не найден, возвращается -1. if j == -1 or nums\[j\] != target:
>
> return -1
>
> \# target найден, возвращается индекс j. return j
##### Преобразование в поиск элемента
> Известно, что, когда массив не содержит элемент target, индексы *i* и *j* в конеч- ном итоге указывают на первый элемент, больший target, и на первый эле- мент, меньший target, соответственно.
>
> Таким образом, для поиска левой и правой границ можно создать элемент, отсутствующий в массиве, как показано на рис. 10.8.
- **Поиск самого левого** target: можно преобразовать в поиск target - 0.5
> и вернуть указатель *i*.
- **Поиск самого правого** target: можно преобразовать в поиск target + 0.5
> и вернуть указатель *j*.
>
> ![](ru/docs/assets/media/image551.jpeg)
>
> **Рис. 10.8.** Преобразование поиска границ в поиск элемента
>
> Код мы не приводим, но стоит обратить внимание на следующие два мо- мента:
1) данный массив не содержит дробных чисел, т. е. не нужно беспокоиться об обработке случаев равенства другим элементам массива;
2) поскольку этот метод вводит дробные числа, необходимо изменить тип переменной target на тип с плавающей запятой (в Python это изменение не требуется).
#### Стратегии оптимизации хеширования
> В алгоритмических задачах **линейный поиск часто заменяется на хеш- поиск**, **чтобы снизить временную сложность алгоритма**. Рассмотрим за- дачу для углубленного понимания этого приема.
### Линейный поиск: обмен времени на пространство
> Рассмотрим *прямой перебор* всех возможных комбинаций. Мы запускаем два вложенных цикла и на каждой итерации проверяем, равна ли сумма двух це- лых чисел target. Если да, то возвращаем их индексы, см. рис. 10.9.
>
> ![](ru/docs/assets/media/image553.jpeg)
>
> **Рис. 10.9.** Линейный поиск для нахождения двух чисел, сумма которых равна заданному
>
> Ниже приведен код реализации.
>
> \# === File: two_sum.py ===
>
> def two_sum_brute_force(nums: list\[int\], target: int) -\> list\[int\]: \"\"\" Метод 1: Полный перебор.\"\"\"
>
> \# Два вложенных цикла, временная сложность O(n\^2). for i in range(len(nums) - 1):
>
> for j in range(i + 1, len(nums)): if nums\[i\] + nums\[j\] == target:
>
> return \[i, j\]
>
> return \[\]
>
> Временная сложность этого метода составляет O(*n*2), а пространственная сложность *O*(1), что делает его крайне медленным при большом объеме данных.
### Хеш-поиск: обмен пространства на время
> Рассмотрим использование *хеш-таблицы*, в которой ключами и значениями являются элементы массива и их индексы. Циклически обходим массив, вы- полняя следующие шаги, показанные на рис. 10.10:
1) проверить, содержится ли число target - nums\[i\] в хеш-таблице. Если да, то сразу вернуть индексы этих двух элементов;
2) добавить в хеш-таблицу пару ключ--значение: nums\[i\] и *i*.
> ![](ru/docs/assets/media/image555.jpeg)
![](ru/docs/assets/media/image557.jpeg)
> **Рис. 10.10.** Использование вспомогательной хеш-таблицы для нахождения двух чисел, сумма которых равна заданному. Шаги 1--2
>
> ![](ru/docs/assets/media/image559.jpeg)
>
> **Рис. 10.10.** *Окончание*. Шаг 3
>
> Код реализации представлен ниже, требуется только один цикл.
>
> \# === File: two_sum.py ===
>
> def two_sum_hash_table(nums: list\[int\], target: int) -\> list\[int\]: \"\"\" Метод 2: Вспомогательная хеш-таблица.\"\"\"
>
> \# Вспомогательная хеш-таблица, пространственная сложность O(n). dic = {}
>
> \# Один цикл, временная сложность O(n). for i in range(len(nums)):
>
> if target - nums\[i\] in dic:
>
> return \[dic\[target - nums\[i\]\], i\] dic\[nums\[i\]\] = i
>
> return \[\]
>
> Этот метод снижает временную сложность с *O*(*n*2) до *O*(*n*) благодаря хеш- поиску, значительно повышая эффективность выполнения.
>
> Поскольку требуется поддерживать дополнительную хеш-таблицу, про- странственная сложность составляет *O*(*n*). **Тем не менее общая эффектив- ность этого метода более сбалансирована**, **что делает его оптимальным решением данной задачи**.
#### переосмысление алгоритмов поиска
> Алгоритмы поиска используются для нахождения одного или нескольких эле- ментов, удовлетворяющих определенным условиям, в структурах данных, та- ких как массивы, списки, деревья или графы.
>
> Алгоритмы поиска можно классифицировать по принципу их реализации на следующие категории.
- **Поиск целевого элемента путем обхода структуры данных**, напри- мер обход массива, списка, дерева и графа.
- **Эффективный поиск элементов с использованием структуры орга- низации данных или априорной информации**, например двоичный поиск, хеш-поиск и поиск в двоичных деревьях.
> Нетрудно заметить, что эти темы уже были рассмотрены в предыдущих гла- вах, поэтому алгоритмы поиска нам уже знакомы. В этом разделе мы система- тизируем полученные ранее знания.
### Полный перебор
> *Полный перебор* заключается в обходе каждого элемента структуры данных для нахождения целевого элемента.
- Линейный поиск применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не будет найден целевой элемент или не будет достигнут другой конец.
- Поиск в ширину и поиск в глубину -- это две стратегии обхода графов и деревьев. Поиск в ширину начинается с начального узла и исследу- ет все узлы на текущем уровне перед переходом на следующий. Поиск в глубину начинается с начального узла и следует по пути до конца, за- тем возвращается и пробует другие пути, пока не будет полностью прой- дена вся структура данных.
> Преимущество полного перебора заключается в его простоте и универсаль- ности, **так как он не требует предварительной обработки данных и ис- пользования дополнительных структур данных**.
>
> Однако **временная сложность таких алгоритмов составляет** *O*(*n*), где *n* -- количество элементов, что делает их менее эффективными при большом объ- еме данных.
### Адаптивный поиск
> *Адаптивный поиск* использует специфические свойства данных (например, упорядоченность) для оптимизации процесса поиска, что позволяет более эф- фективно находить целевой элемент.
- Двоичный поиск использует упорядоченность данных для эффективно- го поиска и применим только к массивам.
- Хеш-поиск использует хеш-таблицы для создания отображения между данными поиска и целевыми данными, что позволяет эффективно вы- полнять операции поиска.
- Поиск в дереве осуществляется в определенной структуре дерева (на- пример, в двоичном дереве поиска) путем сравнения значений узлов для быстрого исключения узлов и нахождения целевого элемента.
> Преимуществом таких алгоритмов является высокая эффективность, **вре- менная сложность может достигать** *O*(log *n*) **и даже** *O*(1).
>
> Однако **использование этих алгоритмов часто требует предваритель- ной обработки данных**. Например, для двоичного поиска необходимо пред-
>
> варительно отсортировать массив, хеш-поиск и поиск в дереве требуют ис- пользования дополнительных структур данных, поддержание которых также требует дополнительных временных и пространственных затрат.
### Выбор метода поиска
> Для поиска целевого элемента в заданном наборе данных размером *n* можно использовать различные методы, такие как линейный поиск, двоичный поиск, поиск в дереве, хеш-поиск и др. Принципы работы каждого метода показаны на рис. 10.11.
![](ru/docs/assets/media/image561.jpeg)
> **Рис. 10.11.** Различные стратегии поиска
>
> Эффективность и характеристики указанных методов приведены в табл. 10.1.
+---------------------+----------------------+----------------+----------------------+-----------------+
| | > **Линейный поиск** | > **Двоичный** | > **Поиск в дереве** | > **Хеш-поиск** |
+=====================+======================+================+======================+=================+
| > Поиск элемента | > *O*(*n*) | > *O*(log *n*) | > *O*(log *n*) | > *O*(1) |
+---------------------+----------------------+----------------+----------------------+-----------------+
| > Вставка элемента | > *O*(1) | > *O*(*n*) | > *O*(log *n*) | > *O*(1) |
+---------------------+----------------------+----------------+----------------------+-----------------+
| > Удаление элемента | > *O*(*n*) | > *O*(*n*) | > *O*(log *n*) | > *O*(1) |
+---------------------+----------------------+----------------+----------------------+-----------------+
| > Дополнительное | > *O*(1) | > *O*(1) | > *O*(*n*) | > *O*(*n*) |
+---------------------+----------------------+----------------+----------------------+-----------------+
> **поиск**
>
> пространство
+------------------------------------+----------------------+--------------------+----------------------------------------+------------------------------------+
| | | | | > *Окончание табл. 10.1* |
+====================================+======================+:==================:+========================================+:==================================:+
| | > **Линейный поиск** | > **Двоичный** | > **Поиск в дереве** | > **Хеш-поиск** |
+------------------------------------+----------------------+--------------------+----------------------------------------+------------------------------------+
| > Предварительная обработка данных | > -- | > Сортировка | > Построение де- рева *O*(*n* log *n*) | > Построение хеш- таблицы *O*(*n*) |
| | | > | | |
| | | > *O*(*n* log *n*) | | |
+------------------------------------+----------------------+--------------------+----------------------------------------+------------------------------------+
| > Упорядоченность данных | > Неупорядо- ченные | > Упорядочен- | > Упорядоченные | > Неупорядоченные |
+------------------------------------+----------------------+--------------------+----------------------------------------+------------------------------------+
> ные
>
> Выбор алгоритма поиска также зависит от объема данных, требований к производительности поиска, частоты запросов и обновлений данных.
##### Линейный поиск
- Обладает хорошей универсальностью, не требует предварительной об- работки данных. Если необходимо выполнить только один запрос, вре- мя предварительной обработки данных для других трех методов будет дольше, чем время линейного поиска.
- Подходит для небольших объемов данных, в этом случае временная сложность мало влияет на эффективность.
- Подходит для сценариев с высокой частотой обновления данных, так как этот метод не требует дополнительного обслуживания данных.
##### Двоичный поиск
- Подходит для больших объемов данных, демонстрирует стабильную эф- фективность, худшая временная сложность составляет *O*(log *n*).
- Объем данных не должен быть слишком большим, так как массив требует непрерывного пространства в памяти.
- Не подходит для сценариев с частыми добавлениями и удалениями дан- ных, так как поддержание упорядоченного массива требует значитель- ных затрат.
##### Хеш-поиск
- Подходит для сценариев с высокими требованиями к производительно- сти поиска, средняя временная сложность составляет *O*(1).
- Не подходит для случаев, когда требуется упорядоченность данных или поиск по диапазону, так как хеш-таблица не может поддерживать упоря- доченность данных.
- Сильно зависит от хеш-функции и стратегии обработки коллизий, суще- ствует значительный риск ухудшения производительности.
- Не подходит для слишком больших объемов данных, так как хеш-таблица требует дополнительного пространства для минимизации коллизий и обеспечения хорошей производительности поиска.
##### Поиск в дереве
- Подходит для огромных объемов данных, так как узлы дерева хранятся в памяти раздельно.
- Подходит для случаев, когда требуется поддержание упорядоченности данных или поиск по диапазону.
<!-- -->
- В процессе постоянного добавления и удаления узлов двоичное дерево поиска может стать несбалансированным, и временная сложность ухуд- шится до *O*(*n*).
- Если используется АВЛ-дерево или красно-черное дерево, все операции могут выполняться со стабильной эффективностью *O*(log *n*), но операции по поддержанию баланса дерева увеличивают дополнительные затраты.
#### резюме
- Двоичный поиск требует упорядоченности данных и выполняется путем циклического сокращения области поиска в два раза. Требует упорядо- ченности входных данных и подходит только для массивов или структур данных, основанных на массивах.
- Полный перебор осуществляется путем обхода структуры данных для нахождения целевого значения. Линейный поиск подходит для массивов и списков, поиск в ширину и поиск в глубину подходят для графов и де- ревьев. Эти алгоритмы обладают хорошей универсальностью, не требу- ют предварительной обработки данных, но их временная сложность *O*(*n*) достаточно высока.
- Хеш-поиск, поиск в деревьях и двоичный поиск относятся к эффек- тивным методам поиска, которые позволяют быстро находить целевой элемент в определенных структурах данных. Эти алгоритмы отличают- ся высокой эффективностью, их временная сложность может достигать *O*(log *n*) или даже *O*(1), однако обычно они требуют использования до- полнительных структур данных.
- На практике для выбора подходящего метода необходимо проводить конкретный анализ таких факторов, как объем данных, требования к производительности поиска, частота запросов и обновлений данных.
- Линейный поиск подходит для небольших или часто обновляемых дан- ных. Двоичный поиск -- для больших, отсортированных данных. Хеш- поиск -- когда важна высокая эффективность запросов и не требуется поиск диапазонов. Поиск в деревьях -- для больших динамических дан- ных, в которых необходимо поддерживать порядок и выполнять запросы диапазонов.
- Замена линейного поиска на хеш-поиск является распространенной стратегией оптимизации времени выполнения, позволяющей снизить временную сложность с *O*(*n*) до *O*(*1*).
> Глава 11