Поиск в двоичном дереве поиска: алгоритм и сложность
Двоичное дерево поиска (BST, binary search tree) - одна из базовых структур данных, которую разбирают в каждом курсе по алгоритмам. Суть простая: в каждом узле хранится ключ, все ключи левого поддерева меньше него, все ключи правого - больше. Эта инвариантность позволяет искать, вставлять и удалять элементы за в среднем случае, не просматривая всё дерево целиком. Чтобы сразу увидеть, как изменяется число сравнений при разных размерах дерева и разных целевых ключах, покрутите калькулятор ниже.
Как работает алгоритм поиска в BST
Поиск начинается с корня и на каждом шаге делает одно сравнение:
- Если дерево пусто - ключ не найден, возвращаем
null. - Сравниваем искомый ключ с ключом текущего узла .
- - узел найден, возвращаем его.
- - переходим в левое поддерево.
- - переходим в правое поддерево.
- Повторяем с нового текущего узла.
В итоге алгоритм идёт по одному пути от корня до нужного узла (или до пустого места, если ключа нет). Никакие другие ветки не просматриваются - в этом вся сила BST. На псевдокоде рекурсивная версия выглядит так:
search(node, k):
if node == null: return null
if k == node.key: return node
if k < node.key: return search(node.left, k)
else: return search(node.right, k)
Итеративный вариант делает то же самое без рекурсии: заводит переменную cur = root и в цикле смещает её влево или вправо, пока не найдёт ключ или не дойдёт до null.
Сложность поиска: O(log n) и O(n)
Число сравнений в поиске равно глубине целевого узла плюс один (корень находится на глубине 0, его проверка - первое сравнение). Глубина зависит от формы дерева.
Сбалансированное дерево. Если при каждом разветвлении дерево делится примерно пополам, высота . Поиск занимает от 1 сравнения (корень) до сравнений (лист). Среднее по всем ключам:
где - число узлов на уровне . Для полного BST из узлов среднее приближённо равно .
Вырожденное дерево (цепочка). Если ключи вставлялись в порядке возрастания или убывания, каждый новый узел становится правым (или левым) ребёнком предыдущего. Дерево превращается в связный список, высота равна , а поиск в худшем случае требует сравнений - .

Вот почему порядок вставки элементов в BST критически важен: одни и те же ключи, добавленные в разном порядке, дают деревья с кардинально разной производительностью. Эту проблему решают самобалансирующиеся деревья - AVL-дерево и красно-чёрное дерево.
Путь поиска: пример для BST[1..15]
Рассмотрим сбалансированный BST с ключами от 1 до 15. Корень - ключ 8 (середина), его левый ребёнок - 4, правый - 12, и так далее до листьев.
Ищем ключ 11:
- Корень 8: → идём вправо.
- Узел 12: → идём влево.
- Узел 10: → идём вправо.
- Узел 11: → найден.
Потребовалось 4 сравнения при высоте дерева . Максимально возможное число сравнений - , и мы в него попали, потому что 11 - это лист. Если искать корень (8), хватит одного сравнения.
Инвариант BST и почему он работает
BST работает благодаря строгому инварианту: для любого узла все ключи в левом поддереве строго меньше , а все ключи в правом поддереве строго больше. Этот инвариант гарантирует, что на каждом шаге поиска мы точно знаем, в какую половину дерева идти. При равенстве ключей (дубликаты) разные реализации поступают по-разному: кладут дубликат влево, вправо или запрещают его. Важно, чтобы выбранная политика соблюдалась при всех операциях - иначе инвариант нарушится и поиск будет давать неверный результат.
Простая итеративная реализация на Python:
class Node:
def __init__(self, key):
self.key = key
self.left = self.right = None
def search(root, k):
cur = root
while cur:
if k == cur.key:
return cur
cur = cur.left if k < cur.key else cur.right
return None
Успешный и безуспешный поиск
Поиск бывает успешным (ключ найден) и безуспешным (ключа нет). В безуспешном случае алгоритм доходит до null и возвращает «не найдено». Число сравнений при безуспешном поиске равно высоте узла, в поддерево которого «упал бы» искомый ключ, плюс один. В сбалансированном BST это тоже .
Асимптотика в таблице:
| Случай | Сбалансированный BST | Вырожденный BST |
|---|---|---|
| Лучший (корень) | ||
| Средний | ||
| Худший (лист/нет) |
Частые ошибки
- Путают BST с кучей. В BST левый потомок меньше родителя и правого. В куче (heap) каждый родитель больше обоих потомков, но порядок между левым и правым потомками не задан - поиск произвольного ключа в куче работает за , а не за .
- Забывают про вырождение. Если вставлять элементы по возрастанию, получается список. Многие думают, что BST всегда даёт , - это справедливо только для сбалансированных деревьев.
- Некорректный инвариант при дубликатах. Если не определить однозначную политику для равных ключей (влево или вправо), поиск может пропустить существующий элемент.
- Путают глубину и высоту. Глубина узла - расстояние от корня до него. Высота дерева - максимальная глубина листа. Число сравнений при поиске равно глубине узла плюс один.
- Рекурсивная реализация без базового случая. Если не проверить
node == nullв начале, рекурсия упадёт сNullPointerExceptionпри попытке обратиться к.leftпустого узла.
FAQ
Чем BST отличается от бинарного поиска по массиву? Бинарный поиск по отсортированному массиву тоже работает за , но вставка и удаление элемента занимают - нужно сдвигать элементы. В BST вставка и удаление - для сбалансированного дерева, но поиск требует обхода указателей, что медленнее доступа по индексу в массиве из-за кэш-промахов.
Что такое AVL-дерево и зачем оно нужно? AVL-дерево - это BST с дополнительным инвариантом: для каждого узла высоты левого и правого поддеревьев различаются не более чем на 1. После каждой вставки или удаления выполняются повороты, восстанавливающие баланс. Это гарантирует высоту и, следовательно, поиск за в любом случае, без риска вырождения.
Как найти минимальный и максимальный ключ в BST?
Минимальный ключ - в самом левом узле: идём влево от корня, пока не дойдём до null. Максимальный - в самом правом. Обе операции выполняются за шагов, то есть для сбалансированного дерева.
Коротко
Поиск в двоичном дереве поиска движется от корня к листьям, на каждом шаге делая одно сравнение и уходя в левое или правое поддерево. Для сбалансированного BST из узлов высота , число сравнений - от 1 до , среднее . Вырожденное дерево (цепочка) деградирует до сравнений. Самобалансирующиеся структуры (AVL, красно-чёрное дерево) гарантируют независимо от порядка вставки.
Читайте также

AVL-дерево: как работает балансировка и ротации
Разбираем, как AVL-дерево восстанавливает баланс после вставки и удаления: инвариант высоты, balance factor и четыре ротации LL, RR, LR, RL за O(log n).

Splay-дерево: самобалансирующееся BST с поворотами к корню
Splay-дерево Слейтора и Тарьяна: операция splay через zig, zig-zig и zig-zag, амортизированная сложность O(log n) через potential function и сравнение с AVL/Red-Black.

Высота и глубина дерева: формулы и примеры
Разбираем высоту и глубину дерева в информатике: точные определения, формулы для полного двоичного и вырожденного дерева, примеры задач и типичные ошибки.