Введение
Поиск информации, хранящейся в различных структурах данных, является важной частью практически каждого приложения.
Существует множество различных алгоритмов, которые можно использовать для поиска. Каждый из них имеет разные реализации и напрямую зависит от структуры данных, для которой он реализован.
Умение выбрать нужный алгоритм для конкретной задачи является ключевым навыком для разработчиков. Именно правильно подобранный алгоритм отличает быстрое, надежное и стабильное приложение от приложения, которое падает от простого запроса.
В этой статье:
- Операторы членства (Membership Operators)
- Линейный поиск
- Бинарный поиск
- Улучшенный линейный поиск — Jump Search
- Поиск Фибоначчи
- Экспоненциальный поиск
- Интерполяционный поиск
Операторы членства (Membership Operators)
Алгоритмы развиваются и оптимизируются в результате постоянной эволюции и необходимости находить наиболее эффективные решения для основных проблем в различных областях.
Одной из наиболее распространенных проблем в области компьютерных наук является поиск в коллекции и определение того, присутствует ли данный объект в коллекции или нет.
Почти каждый язык программирования имеет свою собственную реализацию базового алгоритма поиска. Обычно — в виде функции, которая возвращает логическое значение True
или False
, когда элемент найден в данной коллекции элементов.
В Python самый простой способ поиска объекта — использовать операторы членства. Их название связано с тем, что они позволяют нам определить, является ли данный объект членом коллекции.
Эти операторы могут использоваться с любой итерируемой структурой данных в Python, включая строки, списки и кортежи.
in
— возвращаетTrue
, если данный элемент присутствует в структуре данных.not in
— возвращаетTrue
, если данный элемент не присутствует в структуре данных.
>>> 'apple' in ['orange', 'apple', 'grape'] True >>> 't' in 'pythonist' True >>> 'q' in 'pythonist' False >>> 'q' not in 'pythonist' True
Операторов членства достаточно, если нам нужно только определить, существует ли подстрока в данной строке, или пересекаются ли две строки, два списка или кортежа с точки зрения содержащихся в них объектов.
В большинстве случаев помимо определения, наличествует ли элемент в последовательности, нам нужна еще и позиция (индекс) элемента. Используя операторы членства, мы не можем получить ее.
Существует множество алгоритмов поиска, которые не зависят от встроенных операторов и могут использоваться для более быстрого и/или эффективного поиска значений. Кроме того, они могут дать больше информации (например, о позиции элемента в коллекции), а не просто определить, есть ли в коллекции этот элемент.
Линейный поиск
Линейный поиск — это один из самых простых и понятных алгоритмов поиска. Мы можем думать о нем как о расширенной версии нашей собственной реализации оператора in
в Python.
Суть алгоритма заключается в том, чтобы перебрать массив и вернуть индекс первого вхождения элемента, когда он найден:
def LinearSearch(lys, element): for i in range (len(lys)): if lys[i] == element: return i return -1
Итак, если мы используем функцию для вычисления:
>>> print(LinearSearch([1,2,3,4,5,2,1], 2))
То получим следующий результат:
1
Это индекс первого вхождения искомого элемента, учитывая, что нумерация элементов в Python начинается с нуля.
Временная сложность линейного поиска равна O(n). Это означает, что время, необходимое для выполнения, увеличивается с увеличением количества элементов в нашем входном списке lys
.
Линейный поиск не часто используется на практике, потому что такая же эффективность может быть достигнута с помощью встроенных методов или существующих операторов. К тому же, он не такой быстрый и эффективный, как другие алгоритмы поиска.
Линейный поиск хорошо подходит для тех случаев, когда нам нужно найти первое вхождение элемента в несортированной коллекции. Это связано с тем, что он не требует сортировки коллекции перед поиском (в отличие от большинства других алгоритмов поиска).
Бинарный поиск
Бинарный поиск работает по принципу «разделяй и властвуй». Он быстрее, чем линейный поиск, но требует, чтобы массив был отсортирован перед выполнением алгоритма.
Предполагая, что мы ищем значение val
в отсортированном массиве, алгоритм сравнивает val
со значением среднего элемента массива, который мы будем называть mid
.
- Если
mid
— это тот элемент, который мы ищем (в лучшем случае), мы возвращаем его индекс. - Если нет, мы определяем, в какой половине массива мы будем искать
val
дальше, основываясь на том, меньше или больше значениеval
значенияmid
, и отбрасываем вторую половину массива. - Затем мы рекурсивно или итеративно выполняем те же шаги, выбирая новое значение для
mid
, сравнивая его сval
и отбрасывая половину массива на каждой итерации алгоритма.
Алгоритм бинарного поиска можно написать как рекурсивно, так и итеративно. В Python рекурсия обычно медленнее, потому что она требует выделения новых кадров стека.
Поскольку хороший алгоритм поиска должен быть максимально быстрым и точным, давайте рассмотрим итеративную реализацию бинарного поиска:
def BinarySearch(lys, val): first = 0 last = len(lys)-1 index = -1 while (first <= last) and (index == -1): mid = (first+last)//2 if lys[mid] == val: index = mid else: if val<lys[mid]: last = mid -1 else: first = mid +1 return index
Если мы используем функцию для вычисления:
>>> BinarySearch([10,20,30,40,50], 20)
То получим следующий результат, являющийся индексом искомого значения:
1
На каждой итерации алгоритм выполняет одно из следующих действий:
- Возврат индекса текущего элемента.
- Поиск в левой половине массива.
- Поиск в правой половине массива.
Мы можем выбрать только одно действие на каждой итерации. Также на каждой итерации наш массив делится на две части. Из-за этого временная сложность двоичного поиска равна O(log n).
Одним из недостатков бинарного поиска является то, что если в массиве имеется несколько вхождений элемента, он возвращает индекс не первого элемента, а ближайшего к середине:
>>> print(BinarySearch([4,4,4,4,4], 4))
После выполнения этого фрагмента кода будет возвращен индекс среднего элемента:
2
Для сравнения: выполнение линейного поиска по тому же массиву вернет индекс первого элемента:
0
Однако мы не можем категорически утверждать, что двоичный поиск не работает, если массив содержит дубликаты. Он может работать так же, как линейный поиск, и в некоторых случаях возвращать первое вхождение элемента. Например:
>>> print(BinarySearch([1,2,3,4,4,4,5], 4)) 3
Бинарный поиск довольно часто используется на практике, потому что он эффективен и быстр по сравнению с линейным поиском. Однако у него есть некоторые недостатки, такие как зависимость от оператора //
. Существует много других алгоритмов поиска, работающих по принципу «разделяй и властвуй», которые являются производными от бинарного поиска. Некоторые из них мы рассмотрим далее.
Jump Search
Jump Search похож на бинарный поиск тем, что он также работает с отсортированным массивом и использует аналогичный подход «разделяй и властвуй» для поиска по нему.
Его можно классифицировать как усовершенствованный алгоритм линейного поиска, поскольку он зависит от линейного поиска для выполнения фактического сравнения при поиске значения.
В заданном отсортированном массиве мы ищем не постепенно по элементам массива, а скачкообразно. Если у нас есть размер прыжка, то наш алгоритм будет рассматривать элементы входного списка lys
в следующем порядке: lys[0]
, lys[0+jump]
, lys[0+2jump]
, lys[0+3jump]
и так далее.
С каждым прыжком мы сохраняем предыдущее значение и его индекс. Когда мы находим множество значений (блок), где lys[i]
< element < lys[i + jump]
, мы выполняем линейный поиск с lys[i]
в качестве самого левого элемента и lys[i + jump]
в качестве самого правого элемента в нашем множестве:
import math def JumpSearch (lys, val): length = len(lys) jump = int(math.sqrt(length)) left, right = 0, 0 while left < length and lys[left] <= val: right = min(length - 1, left + jump) if lys[left] <= val and lys[right] >= val: break left += jump; if left >= length or lys[left] > val: return -1 right = min(length - 1, right) i = left while i <= right and lys[i] <= val: if lys[i] == val: return i i += 1 return -1
Поскольку это сложный алгоритм, давайте рассмотрим пошаговое вычисление для следующего примера:
>>> print(JumpSearch([1,2,3,4,5,6,7,8,9], 5))
- Jump search сначала определит размер прыжка путем вычисления
math.sqrt(len(lys))
. Поскольку у нас 9 элементов, размер прыжка будет √9 = 3. - Далее мы вычисляем значение переменной
right
. Оно рассчитывается как минимум из двух значений: длины массива минус 1 и значенияleft + jump
, которое в нашем случае будет 0 + 3 = 3. Поскольку 3 меньше 8, мы используем 3 в качестве значения переменнойright
. - Теперь проверим, находится ли наш искомый элемент 5 между
lys[0]
иlys[3]
. Поскольку 5 не находится между 1 и 4, мы идем дальше. - Затем мы снова делаем расчеты и проверяем, находится ли наш искомый элемент между
lys[3]
иlys[6]
, где 6 — это 3 + jump. Поскольку 5 находится между 4 и 7, мы выполняем линейный поиск по элементам междуlys[3]
иlys[6]
и возвращаем индекс нашего элемента:
4
Временная сложность jump search равна O(√n), где √n — размер прыжка, а n — длина списка. Таким образом, с точки зрения эффективности jump search находится между алгоритмами линейного и бинарного поиска.
Единственное наиболее важное преимущество jump search по сравнению с бинарным поиском заключается в том, что он не опирается на оператор деления (/
).
В большинстве процессоров использование оператора деления является дорогостоящим по сравнению с другими основными арифметическими операциями (сложение, вычитание и умножение), поскольку реализация алгоритма деления является итеративной.
Стоимость сама по себе очень мала, но когда количество искомых элементов очень велико, а количество необходимых операций деления растет, стоимость может постепенно увеличиваться. Поэтому jump search лучше бинарного поиска, когда в системе имеется большое количество элементов: там даже небольшое увеличение скорости имеет значение.
Чтобы ускорить jump search, мы могли бы использовать бинарный поиск или какой-нибудь другой алгоритм для поиска в блоке вместо использования гораздо более медленного линейного поиска.
Поиск Фибоначчи
Поиск Фибоначчи — это еще один алгоритм «разделяй и властвуй», который имеет сходство как с бинарным поиском, так и с jump search. Он получил свое название потому, что использует числа Фибоначчи для вычисления размера блока или диапазона поиска на каждом шаге.
Числа Фибоначчи — это последовательность чисел 0, 1, 1, 2, 3, 5, 8, 13, 21 …, где каждый элемент является суммой двух предыдущих чисел.
Алгоритм работает с тремя числами Фибоначчи одновременно. Давайте назовем эти три числа fibM
, fibM_minus_1
и fibM_minus_2
. Где fibM_minus_1
и fibM_minus_2
— это два числа, предшествующих fibM
в последовательности:
fibM = fibM_minus_1 + fibM_minus_2
Мы инициализируем значения 0, 1, 1 или первые три числа в последовательности Фибоначчи. Это поможет нам избежать IndexError
в случае, когда наш массив lys
содержит очень маленькое количество элементов.
Затем мы выбираем наименьшее число последовательности Фибоначчи, которое больше или равно числу элементов в нашем массиве lys
, в качестве значения fibM
. А два числа Фибоначчи непосредственно перед ним — в качестве значений fibM_minus_1
и fibM_minus_2
. Пока в массиве есть элементы и значение fibM
больше единицы, мы:
- Сравниваем
val
со значением блока в диапазоне доfibM_minus_2
и возвращаем индекс элемента, если он совпадает. - Если значение больше, чем элемент, который мы в данный момент просматриваем, мы перемещаем значения
fibM
,fibM_minus_1
иfibM_minus_2
на два шага вниз в последовательности Фибоначчи и меняем индекс на индекс элемента. - Если значение меньше, чем элемент, который мы в данный момент просматриваем, мы перемещаем значения
fibM,
fibM_minus_1
иfibM_minus_2
на один шаг вниз в последовательности Фибоначчи.
Давайте посмотрим на реализацию этого алгоритма на Python:
def FibonacciSearch(lys, val): fibM_minus_2 = 0 fibM_minus_1 = 1 fibM = fibM_minus_1 + fibM_minus_2 while (fibM < len(lys)): fibM_minus_2 = fibM_minus_1 fibM_minus_1 = fibM fibM = fibM_minus_1 + fibM_minus_2 index = -1; while (fibM > 1): i = min(index + fibM_minus_2, (len(lys)-1)) if (lys[i] < val): fibM = fibM_minus_1 fibM_minus_1 = fibM_minus_2 fibM_minus_2 = fibM - fibM_minus_1 index = i elif (lys[i] > val): fibM = fibM_minus_2 fibM_minus_1 = fibM_minus_1 - fibM_minus_2 fibM_minus_2 = fibM - fibM_minus_1 else : return i if(fibM_minus_1 and index < (len(lys)-1) and lys[index+1] == val): return index+1; return -1
Используем функцию FibonacciSearch для вычисления:
>>> print(FibonacciSearch([1,2,3,4,5,6,7,8,9,10,11], 6))
Давайте посмотрим на пошаговый процесс поиска:
- Присваиваем переменной
fibM
наименьшее число Фибоначчи, которое больше или равно длине списка. В данном случае наименьшее число Фибоначчи, отвечающее нашим требованиям, равно 13. - Значения присваиваются следующим образом:
fibM = 13
fibM_minus_1 = 8
fibM_minus_2 = 5
index = -1
- Далее мы проверяем элемент
lys[4]
, где 4 — это минимум из двух значений —index + fibM_minus_2
(-1+5) и длина массива минус 1 (11-1). Поскольку значениеlys[4]
равно 5, что меньше искомого значения, мы перемещаем числа Фибоначчи на один шаг вниз в последовательности, получая следующие значения:
fibM = 8
fibM_minus_1 = 5
fibM_minus_2 = 3
index = 4
- Далее мы проверяем элемент
lys[7]
, где 7 — это минимум из двух значений:index + fibM_minus_2
(4 + 3) и длина массива минус 1 (11-1). Поскольку значениеlys[7]
равно 8, что больше искомого значения, мы перемещаем числа Фибоначчи на два шага вниз в последовательности, получая следующие значения:
fibM = 3
fibM_minus_1 = 2
fibM_minus_2 = 1
index = 4
- Затем мы проверяем элемент
lys[5]
, где 5 — это минимум из двух значений:index + fibM_minus_2
(4+1) и длина массива минус 1 (11-1) . Значениеlys[5]
равно 6, и это наше искомое значение!
Получаем ожидаемый результат:
5
Временная сложность поиска Фибоначчи равна O(log n). Она такая же, как и у бинарного поиска. Это означает, что алгоритм в большинстве случаев работает быстрее, чем линейный поиск и jump search.
Поиск Фибоначчи можно использовать, когда у нас очень большое количество искомых элементов и мы хотим уменьшить неэффективность, связанную с использованием алгоритма, основанного на операторе деления.
Дополнительным преимуществом использования поиска Фибоначчи является то, что он может вместить входные массивы, которые слишком велики для хранения в кэше процессора или ОЗУ, потому что он ищет элементы с увеличивающимся шагом, а не с фиксированным.
Экспоненциальный поиск
Экспоненциальный поиск — это еще один алгоритм поиска, который может быть достаточно легко реализован на Python, по сравнению с jump search и поиском Фибоначчи, которые немного сложны. Он также известен под названиями galloping search, doubling search и Struzik search.
Экспоненциальный поиск зависит от бинарного поиска для выполнения окончательного сравнения значений. Алгоритм работает следующим образом:
- Определяется диапазон, в котором, скорее всего, будет находиться искомый элемент.
- В этом диапазоне используется двоичный поиск для нахождения индекса элемента.
Реализация алгоритма экспоненциального поиска на Python:
def ExponentialSearch(lys, val): if lys[0] == val: return 0 index = 1 while index < len(lys) and lys[index] <= val: index = index * 2 return BinarySearch( lys[:min(index, len(lys))], val)
Используем функцию, чтобы найти значение:
>>> print(ExponentialSearch([1,2,3,4,5,6,7,8],3))
Рассмотрим работу алгоритма пошагово.
- Проверяем, соответствует ли первый элемент списка искомому значению: поскольку
lys[0]
равен 1, а мы ищем 3, мы устанавливаем индекс равным 1 и двигаемся дальше. - Перебираем все элементы в списке, и пока элемент с текущим индексом меньше или равен нашему значению, умножаем значение индекса на 2:
- index = 1,
lys[1]
равно 2, что меньше 3, поэтому значение index умножается на 2 и переменнойindex
присваивается значение 2. - index = 2,
lys[2]
равно 3, что равно 3, поэтому значениеindex
умножается на 2 и переменнойindex
присваивается значение 4. - index = 4,
lys[4]
равно 5, что больше 3. Условие выполнения цикла больше не соблюдается и цикл завершает свою работу.
- Затем выполняется двоичный поиск в полученном диапазоне (срезе)
lys[:4]
. В Python это означает, что подсписок будет содержать все элементы до 4-го элемента, поэтому мы фактически вызываем функцию следующим образом:
>>> BinarySearch([1,2,3,4], 3)
Функция вернет следующий результат:
2
Этот результат является индексом искомого элемента как в исходном списке, так и в срезе, который мы передаем алгоритму бинарного поиска.
Экспоненциальный поиск выполняется за время O(log i), где i — индекс искомого элемента. В худшем случае временная сложность равна O(log n), когда искомый элемент — это последний элемент в массиве (n — это длина массива).
Экспоненциальный поиск работает лучше, чем бинарный, когда искомый элемент находится ближе к началу массива. На практике мы используем экспоненциальный поиск, поскольку это один из наиболее эффективных алгоритмов поиска в неограниченных или бесконечных массивах.
Интерполяционный поиск
Интерполяционный поиск — это еще один алгоритм «разделяй и властвуй», аналогичный бинарному поиску. В отличие от бинарного поиска, он не всегда начинает поиск с середины. Интерполяционный поиск вычисляет вероятную позицию искомого элемента по формуле:
index = low + [(val-lys[low])*(high-low) / (lys[high]-lys[low])]
В этой формуле используются следующие переменные:
- lys — наш входной массив.
- val — искомый элемент.
- index — вероятный индекс искомого элемента. Он вычисляется как более высокое значение, когда значение val ближе по значению к элементу в конце массива (
lys[high]
), и более низкое, когда значение val ближе по значению к элементу в начале массива (lys[low]
). - low — начальный индекс массива.
- high — последний индекс массива.
Алгоритм осуществляет поиск путем вычисления значения индекса:
- Если значение найдено (когда
lys[index] == val
), возвращается индекс. - Если значение
val
меньшеlys[index]
, то значение индекса пересчитывается по формуле для левого подмассива. - Если значение
val
большеlys[index]
, то значение индекса пересчитывается по формуле для правого подмассива.
Давайте посмотрим на реализацию интерполяционного поиска на Python:
def InterpolationSearch(lys, val): low = 0 high = (len(lys) - 1) while low <= high and val >= lys[low] and val <= lys[high]: index = low + int(((float(high - low) / ( lys[high] - lys[low])) * ( val - lys[low]))) if lys[index] == val: return index if lys[index] < val: low = index + 1; else: high = index - 1; return -1
Если мы используем функцию для вычисления:
>>> print(InterpolationSearch([1,2,3,4,5,6,7,8], 6))
Наши начальные значения будут следующими:
val = 6,
low = 0,
high = 7,
lys[low] = 1,
lys[high] = 8,
index = 0 + [(6-1)*(7-0)/(8-1)] = 5
Поскольку lys[5]
равно 6, что является искомым значением, мы прекращаем выполнение и возвращаем результат:
5
Если у нас большое количество элементов и наш индекс не может быть вычислен за одну итерацию, то мы продолжаем пересчитывать значение индекса после корректировки значений high и low.
Временная сложность интерполяционного поиска равна O(log log n), когда значения распределены равномерно. Если значения распределены неравномерно, временная сложность для наихудшего случая равна O(n) — так же, как и для линейного поиска.
Интерполяционный поиск лучше всего работает на равномерно распределенных, отсортированных массивах. В то время как бинарный поиск начинает поиск с середины и всегда делит массив на две части, интерполяционный поиск вычисляет вероятную позицию элемента и проверяет индекс, что повышает вероятность нахождения элемента за меньшее количество итераций.
Python очень удобочитаемый и эффективный по сравнению с такими языками программирования, как Java, Fortran, C, C++ и т. д. Одним из ключевых преимуществ использования Python для реализации алгоритмов поиска является то, что вам не нужно беспокоиться о приведении или явной типизации.
В Python большинство алгоритмов поиска, которые мы обсуждали, будут работать так же хорошо, если мы ищем строку. Имейте в виду, что понадобится внести изменения в код для алгоритмов, которые используют искомый элемент для числовых вычислений, например алгоритм интерполяционного поиска.
Python также подходит, если вы хотите сравнить производительность различных алгоритмов поиска для вашего dataset’а. Создание прототипа на Python проще и быстрее, потому что вы можете сделать больше с меньшим количеством строк кода.
Чтобы сравнить производительность наших реализованных алгоритмов, в Python мы можем использовать библиотеку time:
import time start = time.time() # вызовите здесь функцию end = time.time() print(start-end)
Заключение
Существует множество возможных способов поиска элемента в коллекции. В этой статье мы обсудили несколько алгоритмов поиска и их реализации на Python.
Выбор используемого алгоритма зависит от данных, с которыми вы будете работать. Это ваш входной массив, который мы называли lys
во всех наших реализациях.
- Если вы хотите выполнить поиск в несортированном массиве или найти первое вхождение искомой переменной, то лучшим вариантом будет линейный поиск.
- Если вы хотите выполнить поиск в отсортированном массиве, есть много вариантов, из которых самый простой и быстрый — это бинарный поиск.
- Если у вас есть отсортированный массив, в котором вы хотите выполнить поиск без использования оператора деления, вы можете использовать либо jump search, либо поиск Фибоначчи.
- Если вы знаете, что искомый элемент, скорее всего, находится ближе к началу массива, вы можете использовать экспоненциальный поиск.
- Если ваш отсортированный массив равномерно распределен, то самым быстрым и эффективным будет интерполяционный поиск.
Если вы не уверены, какой алгоритм использовать для отсортированного массива, просто протестируйте каждый из них при помощи библиотеки time и выберите тот, который лучше всего работает с вашим dataset’ом.
Введение В этой статье мы углубимся в идею и реализацию двоичного поиска в Python. Двоичный поиск – это эффективный алгоритм поиска, работающий с отсортированными массивами. Он часто используется как один из первых примеров алгоритмов, которые работают в логарифмическом времени (O (logn)) из-за его интуитивного поведения, и является фундаментальным алгоритмом в компьютерных науках. Двоичный поиск – пример двоичного поиска работает по принципу «разделяй и властвуй» и полагается на то, что массив отсортирован.
Вступление
В этой статье мы погрузимся в идею и реализацию двоичного поиска в
Python.
Двоичный поиск – это эффективный алгоритм поиска, работающий с
отсортированными массивами. Он часто используется как один из первых
примеров алгоритмов, которые работают в логарифмическом времени ( O
(logn) ) из-за его интуитивного поведения, и является фундаментальным
алгоритмом в компьютерных науках.
Двоичный поиск – пример
Двоичный поиск работает по принципу «разделяй и властвуй» и полагается
на тот факт, что массив сортируется для исключения половины возможных
кандидатов на каждой итерации. В частности, он сравнивает средний
элемент отсортированного массива с элементом, который он ищет, чтобы
решить, где продолжить поиск.
Если целевой элемент больше среднего элемента – он не может быть
расположен в первой половине коллекции, поэтому он отбрасывается. То же
самое и наоборот.
Примечание. Если в массиве четное количество элементов, не имеет
значения, с какого из двух «средних» элементов мы начнем.
Давайте быстро рассмотрим пример, прежде чем мы продолжим объяснять, как
работает двоичный поиск:
{.ezlazyload}
Как мы видим, мы точно знаем, что, поскольку массив отсортирован, x не
находится в первой половине исходного массива.
Когда мы знаем, в какой половине исходного массива x находится, мы
можем повторить этот точный процесс с этой половиной и снова разделить
его на половины, отбросив половину, которая наверняка не содержит x :
{.ezlazyload}
Мы повторяем этот процесс, пока не получим подмассив, содержащий только
один элемент. Мы проверяем, является ли этот элемент x . Если это так
- мы нашли x , если нет – x вообще не существует в массиве.
Если вы внимательно посмотрите на это, вы можете заметить, что в худшем
случае ( x не существует в массиве) нам нужно проверить гораздо
меньшее количество элементов, чем нам нужно в несортированном массиве,
что потребует чего-то большего, вроде линейного поиска , что безумно
неэффективно.
Чтобы быть более точным, количество элементов, которые нам нужно
проверить в худшем случае, равно log ~2~ N, где N – количество
элементов в массиве.
Это оказывает большее влияние, чем больше массив:
Если бы в нашем массиве было 10 элементов, нам нужно было бы проверить
только 3 элемента, чтобы либо найти x, либо сделать вывод, что его
там нет. Это 33,3%.Однако, если бы в нашем массиве было 10 000 000 элементов, нам нужно
было бы проверить только 24 элемента. Это 0,0002%.
Реализация двоичного поиска
Двоичный поиск – это естественно рекурсивный алгоритм, поскольку один и
тот же процесс повторяется для все меньших и меньших массивов, пока не
будет найден массив размером 1. Однако, конечно же, существует
итеративная реализация, и мы продемонстрируем оба подхода.
Рекурсивный
Начнем с рекурсивной реализации, поскольку она более естественна:
def binary_search_recursive(array, element, start, end):
if start > end:
return -1
mid = (start + end) // 2
if element == array[mid]:
return mid
if element < array[mid]:
return binary_search_recursive(array, element, start, mid-1)
else:
return binary_search_recursive(array, element, mid+1, end)
Давайте подробнее рассмотрим этот код. Выходим из рекурсии, если start
элемент выше end
:
if start > end:
return -1
Это связано с тем, что такая ситуация возникает только тогда, когда
элемент не существует в массиве. Что происходит, так это то, что у нас
остается только один элемент в текущем подмассиве, и этот элемент не
соответствует тому, который мы ищем.
На этом этапе start
равно end
. Однако, поскольку element
не равен
array[mid]
, мы снова «разбиваем» массив таким образом, что либо
уменьшаем end
на 1, либо увеличиваем start
на единицу, и при этом
условии существует рекурсия.
Мы могли бы сделать это, используя другой подход:
if len(array) == 1:
if element == array[mid]:
return mid
else:
return -1
Остальная часть кода выполняет логику «проверить средний элемент,
продолжить поиск в соответствующей половине массива». Мы находим индекс
среднего элемента и проверяем, соответствует ли ему искомый элемент:
mid = (start + end) // 2
if elem == array[mid]:
return mid
Если нет, мы проверяем, больше ли элемент или меньше среднего:
if element < array[mid]:
# Continue the search in the left half
return binary_search_recursive(array, element, start, mid-1)
else:
# Continue the search in the right half
return binary_search_recursive(array, element, mid+1, end)
Давайте продолжим и запустим этот алгоритм с небольшой модификацией,
чтобы он распечатал, над каким подмассивом он работает в настоящее
время:
element = 18
array = [1, 2, 5, 7, 13, 15, 16, 18, 24, 28, 29]
print("Searching for {}".format(element))
print("Index of {}: {}".format(element, binary_search_recursive(array, element, 0, len(array))))
Выполнение этого кода приведет к:
Searching for 18
Subarray in step 0:[1, 2, 5, 7, 13, 15, 16, 18, 24, 28, 29]
Subarray in step 1:[16, 18, 24, 28, 29]
Subarray in step 2:[16, 18]
Subarray in step 3:[18]
Index of 18: 7
Ясно видно, как он вдвое сокращает пространство поиска на каждой
итерации, приближаясь к элементу, который мы ищем. Если бы мы попытались
найти элемент, которого нет в массиве, результат был бы таким:
Searching for 20
Subarray in step 0: [4, 14, 16, 17, 19, 21, 24, 28, 30, 35, 36, 38, 39, 40, 41, 43]
Subarray in step 1: [4, 14, 16, 17, 19, 21, 24, 28]
Subarray in step 2: [19, 21, 24, 28]
Subarray in step 3: [19]
Index of 20: -1
И просто для удовольствия, мы можем попробовать выполнить поиск в
некоторых больших массивах и посмотреть, сколько шагов требуется
двоичный поиск, чтобы выяснить, существует ли число:
Searching for 421, in an array with 200 elements
Search finished in 6 steps. Index of 421: 169
Searching for 1800, in an array with 1500 elements
Search finished in 11 steps. Index of 1800: -1
Searching for 3101, in an array with 3000 elements
Search finished in 8 steps. Index of 3101: 1551
Итеративный
Итерационный подход очень прост и похож на рекурсивный подход. Здесь мы
просто выполнить проверку в виде в while
цикла:
def binary_search_iterative(array, element):
mid = 0
start = 0
end = len(array)
step = 0
while (start <= end):
print("Subarray in step {}: {}".format(step, str(array[start:end+1])))
step = step+1
mid = (start + end) // 2
if element == array[mid]:
return mid
if element < array[mid]:
end = mid - 1
else:
start = mid + 1
return -1
Давайте заполним массив и найдем в нем элемент:
array = [1, 2, 5, 7, 13, 15, 16, 18, 24, 28, 29]
print("Searching for {} in {}".format(element, array))
print("Index of {}: {}".format(element, binary_search_iterative(array, element)))
Выполнение этого кода дает нам результат:
Searching for 18 in [1, 2, 5, 7, 13, 15, 16, 18, 24, 28, 29]
Subarray in step 0: [1, 2, 5, 7, 13, 15, 16, 18, 24, 28, 29]
Subarray in step 1: [16, 18, 24, 28, 29]
Subarray in step 2: [16, 18]
Subarray in step 3: [18]
Index of 18: 7
Заключение
Двоичный поиск – это невероятный алгоритм для использования с большими
отсортированными массивами или всякий раз, когда мы планируем
многократно искать элементы в одном массиве.
Стоимость однократной сортировки массива и последующего использования
двоичного поиска для поиска элементов в нем несколько раз намного лучше,
чем использование линейного поиска в несортированном массиве, чтобы мы
могли избежать затрат на его сортировку.
Если мы сортируем массив и ищем элемент только один раз, более
эффективно просто выполнить линейный поиск в несортированном массиве.
Если вы хотите прочитать об алгоритмах сортировки в
Python , мы вам поможем!
На чтение 3 мин Просмотров 1.2к. Опубликовано
В Python списки (list) — это структуры данных, которые представляют собой упорядоченные коллекции элементов. Каждый элемент в списке имеет свой индекс, который указывает на его положение в списке. Иногда возникает необходимость узнать индекс определенного элемента в списке. Например, если мы хотим удалить элемент из списка или изменить его значение. В этой статье мы рассмотрим несколько способов, как узнать индекс элемента в списке Python.
Содержание
- Методы для нахождения индекса элемента
- Использование метода index()
- Использование цикла for и функции enumerate()
- Обработка исключений при поиске индекса
Методы для нахождения индекса элемента
Использование метода index()
Метод index()
является встроенным методом для списков в Python, который возвращает индекс первого вхождения элемента в списке. Метод принимает один аргумент, который является элементом, индекс которого мы хотим найти. Метод index()
возвращает индекс первого вхождения указанного элемента в список. Если элемент не найден, будет сгенерировано исключение ValueError
.
Пример использования метода index()
:
fruits = ['apple', 'banana', 'cherry', 'apple', 'banana']
index_of_cherry = fruits.index('cherry')
print(index_of_cherry) # Будет выведено: 2
В данном примере мы создали список fruits
, который содержит несколько элементов. Затем мы вызываем метод index()
и передаем ему строку 'cherry'
в качестве аргумента. Метод находит индекс первого вхождения элемента 'cherry'
в списке и возвращает его. Результат выполнения кода будет равен 2
, так как 'cherry'
является третьим элементом списка, и индексация в Python начинается с 0.
Использование цикла for и функции enumerate()
Другой способ для нахождения индексов элементов в списке — использование цикла for
и функции enumerate()
. Функция enumerate()
пронумеровывает элементы списка и возвращает кортежи из двух значений: индекс элемента и сам элемент.
Пример использования enumerate()
для нахождения индексов элементов списка:
fruits = ['apple', 'banana', 'orange', 'kiwi', 'orange']
for index, fruit in enumerate(fruits):
if fruit == 'orange':
print(f"'orange' найден на позиции {index}")
Этот код выведет следующий результат:
'orange' найден на позиции 2
'orange' найден на позиции 4
В этом примере мы использовали цикл for
для перебора всех элементов списка fruits
. Функция enumerate()
возвращает кортеж (index, fruit)
на каждой итерации цикла, и мы проверяем, является ли fruit
равным 'orange'
. Если это так, мы выводим сообщение с позицией элемента.
Также обратите внимание, что список fruits
содержит два элемента со значением 'orange'
. Функция enumerate()
возвращает индексы обоих элементов, потому что они различаются в списке. Если бы мы использовали метод index()
, он вернул бы только первый индекс, который соответствует первому элементу со значением 'orange'
.
Обработка исключений при поиске индекса
При поиске индекса элемента в списке Python необходимо учитывать возможность того, что элемент может не находиться в списке. В таком случае, при использовании метода index()
, Python выдаст ошибку ValueError: <элемент> не найден в списке
. Это приведёт к остановке выполнения программы.
Чтобы избежать ошибки при поиске элемента, можно обернуть операцию поиска в блок try-except
, чтобы обработать возможное исключение. Если элемент не найден, в блоке except
можно выполнить определенные действия, например, вывести сообщение об ошибке или вернуть значение по умолчанию.
Пример использования обработки исключений при поиске индекса элемента:
my_list = [1, 2, 3, 4, 5]
try:
index = my_list.index(6)
print(f"Индекс элемента: {index}")
except ValueError:
print("Элемент не найден в списке")
В этом примере мы пытаемся найти индекс элемента 6
в списке my_list
. Так как элемент не найден в списке, возникает исключение ValueError
. Мы обрабатываем это исключение с помощью блока try-except
и выводим сообщение об ошибке.
Если мы попробуем найти индекс элемента, который присутствует в списке, то мы получим корректный результат.
20 Янв 2023 ,
1455
В этой статье мы рассмотрим Двоичный (бинарный) поиск – один из классических алгоритмов поиска элемента в отсортированном массиве.
Предположим вы знаете в каком месяце родился ваш друг(пусть это будет сентябрь) , но не знаете в какой день. И вам нужно за наименьшее количество попыток определить в какой день месяца он родился. И при этом вам ваш друг на каждую вашу попытку будет давать один их трех ответов: “мало”, “много” или “угадал”
Самый простой способ это метод простого поиска. Вы перебираете все варианты подряд 1, 2, 3 и далее.Конечно , если ваш друг родился 3 сентября вам понадятся всего лишь три попытки , но если 24 сентября , то вам нужно 24 попытки. А если друг родился 30 сентября , то нужны все 30 попыток.
Рассмотрим другой более эффективный метод поиска.
Допустим , что у нашего друга день рождения 24 сентября.
Так как исходный массив содержит 30 элементов , то мы на первом шаге назовем элемент который находится посередине этого массива. Это число 15. Наш друг говорит , что “мало”. И теперь мы понимаем , что все числа меньше 15 и само число 15 можно исключить из поиска и можно сосредоточиться на числах, которые больше 15.
На втором шаге мы назовем число которое находится посередине чисел от 16 до 30 и этим числом является 23 , но это тоже меньше искомого числа. И поэтому мы исключаем все числа меньше 23 и само число 23.
И так мы повторяем пока не найдем искомое число.В нашем случае это число 24.
Ниже на рисунке для наглядности показаны все шаги нахождения искомого числа(число 24) методом бинарного поиска
Чтобы найти число 24 в массиве из 30 элементов при помощи простого поиска нам нужно 24 попытки , то при помощи бинарного поиска нам потребовалось всего лишь 5 попыток. То есть в массиве из 30 элементов мы гарантированно за 5 попыток можем найти любой элемент, так log 30 примерно будет равно 5.
А что будет если у нас количество элементов равно не 30 , а скажем 1000? То тогда при простом поиске(переборе) , чтобы найти число 1000 нужно сделать 1000 итераций(попыток). А вот при бинарном поиске нужно сделать всего лишь 10 попыток , потому что 2 в 10 степени это 1024. Для 10 тысячи элементов при бинарном поиске нужно всего лишь 14 попыток , а вот при переборе количество попыток растет линейно. В этом и мощь бинарного алгоритма и его широкое распространение.
O(log n) — логарифмическая сложность для бинарного поиска
O(n) — Линейная сложность для простого поиска
Сравнение линейной и логарифмической функций.
Реализация алгоритма бинарного поиска на Python.
def binary_search(sequence, start_element, key):
end_element = len(sequence) - 1
while start_element <= end_element:
middle_element = start_element + (end_element - start_element) // 2
if sequence[middle_element] == key:
return middle_element
elif sequence[middle_element] < key:
start_element = middle_element + 1
else:
end_element = middle_element - 1
return -1
# Можно было еще элегантно создать sequence = [i for i range(1, 31)],
# но для наглядности перечислил все элементы
sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
# Число которое мы ищем
find_element = 24
result = binary_search(sequence=sequence, start_element=0, key=find_element)
print(result)
Заключение
В данной статье мы рассмотрели один из классических алгоритмов поиска. Это бинарный поиск элемента в отсортированном массиве.
Другие статьи в блоге
Уровни изоляции транзакций в Postgresql
Django Rest Framework (DRF) – Загрузка файлов
Python словари. Введение и основные методы работы с ними
Введение SQLAlchemy Core и ORM
Poetry. Управление зависимостями для проектов на Python
comments powered by