# Поиск ![](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