Бинарный поиск в массиве (Python, два фиктивных элемента)

Сайт: Информатикс
Курс: Зимняя школа в "Эврике"
Книга: Бинарный поиск в массиве (Python, два фиктивных элемента)
Напечатано:: Гость
Дата: Пятница, 27 Июнь 2025, 04:05

1. Линейный поиск в массиве.

Рассмотрим сначала задачу линейного поиска элемента в массиве. 
Необходимо реализовать функцию, которая проверяет, содержится ли в данном списке A данный элемент key. 
Функция будет возвращать значение True или False.
Это можно сделать при помощи цикла for с проверкой условия внутри цикла:

def search(A, key):
    for i in range(len(A)):
        if A[i] == key:
            return True
    return False

Возможна реализация, использующая цикл while без условия внутри: 

def search(A, key):
    i = 0
    while i < len(A) and A[i] != key:
        i += 1
    return i < len(A)

Также каждая из этих функций может возвращать индекс найденного элемента или специальное значение 
(например, −1), если элемент не найден:

def search(A, key):
    for i in range(len(A)):
        if A[i] == key:
            return i
    return -1

Возможна реализация, использующая цикл while:

def search(A, key):
    i = 0
    while i < len(A) and A[i] != key:
        i += 1
    if i < len(A):
        return i
    else:
        return -1

Поскольку все эти алгоритмы должны просмотреть весь список в поисках данного элемента, 
то сложность работы данных алгоритмов будет O(n) (где n — длина списка).

2. Бинарный поиск

Но если исходный массив уже отсортирован, то элемент в нем можно найти гораздо быстрее, если воспользоваться идеей двоичного (бинарного) 
поиска. Идея заключается в делении списка пополам, после чего в зависимости от значения медианного элемента в списке мы переходим либо к 
левой, либо к правой половине списка. Тем самым, длина части, в которой мы ищем элемент, сокращается в два раза на каждом шаге цикла, а, 
значит, общая сложность алгоритма двоичного поиска будет O(log2n).

Итак, перед нами стоит задача — выяснить, содержится ли элемент key в некотором списке, или в его части. 
Мы будем сокращать часть списка, в которой мы ищем элемент key. А именно, введем две границы — left и right. 
При этом мы будем знать, что элемент A[right] строго больше, чем key, то же самое можно сказать и про элементы, которые правее right. 
Про элемент A[left] и те, которые находятся левее него мы будем знать, что все они меньше или равны key. 
А вот про элементы, которые лежат строго между A[left] и A[right] (то есть про элементы, чьи индексы больше left, но меньше right), мы ничего 
не знаем.
В самом начале мы ничего не знаем про все элементы массива, поэтому присвоим left = -1 и right = len(A). 
Можно представить это так — к концам массива добавляются два фиктивных элемента, в левый конец добавляется элемент, в который записывается 
минус бесконечность (т. е. значение, заведомо меньшее, чем key), и этот элемент имеет индекс -1, а в правый элемент дописывается элемент, 
равный плюс бесконечности, и его индекс равен len(A). 
Соответственно, переменные left и right первоначально указывают на эти фиктивные элементы (то есть на самом деле никаких элементов к 
массиву добавлять не требуется, мы это делаем лишь мысленно).
Затем разделим отрезок от left до right на две части и возьмем средний элемент между ними. Его индекс равен middle = (left + right) // 2. 
Сравним значение этого элемента со значением key. 
Если A[middle] строго больше чем key это означает, что сам элемент A[middle] и все, что правее него, должно попасть в правую часть. 
Это означает, что нужно сделать присваивание right = middle. 
Иначе (если A[middle] <= key), то элемент A[middle] и все, что левее него, должно попасть в левую часть, то есть необходимо присвоить 
left = middle.

Будем повторять этот процесс, пока между двумя границами left и right еще есть элементы, то есть пока right > left + 1.

Получаем следующий алгоритм:

left = -1 
right = len(A) 
while right > left + 1: 
    middle = (left + right) // 2 
    if A[middle] > key: 
        right = middle 
    else: 
        left = middle

Что будет после завершения этого алгоритма? 
left и right указывают на два соседних элемента, при этом A[right] > key, A[left] <= key. 
Таким образом, если элемент key содержится в списке, то A[left] == key. 
Правда, возможна ситуация, когда left == -1 (если в списке A все элементы строго больше key), поэтому для ответа на вопрос «содержится ли 
в списке A элемент key» необходимо проверить условие:
left >= 0 and A[left] == key

Что же можно сказать про значение right? 
Это минимальный элемент списка, который строго больше, чем key. 
Иными словами, на место элемента A[right] можно вставить элемент со значением key (сдвинув при этом всю правую часть списка на один элемент), сохраняя упорядоченность списка, при этом right есть самая правая позиция, куда можно вставить в список элемент key, сохраняя упорядоченность.

В этом случае говорят, что значение right является «верхней границей» для элемента key: 
правее этой позиции нельзя вставить элемент key, сохраняя список упорядоченным. 
Вот функция, которая в заданном списке A находит «верхнюю границу» для заданного элемента key:

def UpperBound(A, key): 
    left = -1 
    right = len(A) 
    while right > left + 1: 
        middle = (left + right) // 2 
        if A[middle] > key: 
            right = middle 
        else: 
            left = middle 
    return right

3. Модификация двоичного поиска

Теперь модифицируем алгоритм двоичного поиска так, чтобы элементы, в точности равные key, оказывались в правой части списка, 
а не в левой, как раньше. 
Это потребует изменения неравенства в условии внутри цикла:

def LowerBound(A, key): 
    left = -1 
    right = len(A) 
    while right > left + 1: 
        middle = (left + right) // 2 
        if A[middle] >= key: 
            right = middle 
        else: 
            left = middle 
    return right

На каких значениях left и right закончится данный цикл? A[left] < key, A[right] >= key. 
То есть элемент, равный key, можно вставить на место A[right], сохраняя упорядоченность списка, и это минимальная позиция, 
куда его можно вставить.
Иными словами, данная функция возвращает «нижнюю границу» для элемента key.

Следует отметить, что поскольку UpperBound возвращает индекс первого элемента, который больше чем key, а LowerBound возвращает 
индекс первого элемента, который равен key (а если такого нет — то того, который больше, чем key), то значение разности 
UpperBound(A, key) – LowerBound(A, key) равно числу вхождений элемента key в список A.

При помощи LowerBound также можно проверить вхождение элемента key в список A при помощи условия 
right < len(A) and A[right] == key