mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-05 03:52:41 +08:00
544 lines
47 KiB
Markdown
544 lines
47 KiB
Markdown
# Поиск
|
||
|
||
{width="3.5416633858267716in" height="4.583333333333333in"}
|
||
|
||
#### двоичный поиск
|
||
|
||
> Двоичный (бинарный) поиск -- это эффективный алгоритм поиска, основан- ный на стратегии «разделяй и властвуй». Он использует упорядоченность дан- ных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или область поиска не станет пустой.
|
||
|
||

|
||
|
||
> **Рис. 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.
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 10.2.** Процесс двоичного поиска. Шаги 1--3
|
||
|
||

|
||
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 10.2.** *Продолжение*. Шаги 4--6
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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* также симметричны. Это снижает вероятность ошибок, поэтому обыч- но **рекомендуется использовать запись «двойной замкнутый интервал»**.
|
||
>
|
||
> **Элемент**
|
||
>
|
||
> Индекс
|
||
>
|
||
> Обе границы включены в интервал
|
||
>
|
||
> Интервал поиска: двойной замкнутый \[**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* невелик, линейный поиск ока- зывается быстрее двоичного.
|
||
|
||
#### вставка с использованием двоичного поиска
|
||
|
||
> Двоичный поиск можно использовать не только для поиска целевого элемен- та, но и для решения множества других задач, таких как поиск позиции для вставки целевого элемента.
|
||
|
||
### Случай без повторяющихся элементов
|
||
|
||

|
||
|
||
> **Рис. 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.
|
||
|
||

|
||
|
||
> **Рис. 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* **является точкой вставки**.
|
||
|
||

|
||
|
||
> **Рис. 10.6.** Этапы двоичного поиска точки вставки для повторяющихся элементов. Шаги 1--2
|
||
|
||

|
||
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 10.6.** *Продолжение*. Шаги 3--5
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 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.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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*.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 10.8.** Преобразование поиска границ в поиск элемента
|
||
>
|
||
> Код мы не приводим, но стоит обратить внимание на следующие два мо- мента:
|
||
|
||
1) данный массив не содержит дробных чисел, т. е. не нужно беспокоиться об обработке случаев равенства другим элементам массива;
|
||
|
||
2) поскольку этот метод вводит дробные числа, необходимо изменить тип переменной target на тип с плавающей запятой (в Python это изменение не требуется).
|
||
|
||
#### Стратегии оптимизации хеширования
|
||
|
||
> В алгоритмических задачах **линейный поиск часто заменяется на хеш- поиск**, **чтобы снизить временную сложность алгоритма**. Рассмотрим за- дачу для углубленного понимания этого приема.
|
||
|
||
### Линейный поиск: обмен времени на пространство
|
||
|
||
> Рассмотрим *прямой перебор* всех возможных комбинаций. Мы запускаем два вложенных цикла и на каждой итерации проверяем, равна ли сумма двух це- лых чисел target. Если да, то возвращаем их индексы, см. рис. 10.9.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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*.
|
||
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 10.10.** Использование вспомогательной хеш-таблицы для нахождения двух чисел, сумма которых равна заданному. Шаги 1--2
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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.
|
||
|
||

|
||
|
||
> **Рис. 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
|