Двоичное дерево поиска (BST)
Предпосылки: оценка сложности в O(…), бинарное дерево (left/right, обходы — особенно in-order).
Представим телефонную книгу на 10 000 контактов. Нужно быстро найти номер по имени, добавить новый контакт и удалить старый. Неотсортированный массив даёт O(1) на вставку, но O(n) на поиск — 10 000 сравнений для одного запроса. Отсортированный массив ускоряет поиск до O(log n) через бинарный поиск, но вставка требует сдвига O(n) элементов, чтобы не сломать порядок. BST совмещает оба преимущества: поиск, вставка и удаление выполняются за O(h), где h — высота дерева. Если дерево близко к сбалансированному, h ≈ log₂(n) ≈ 14 для 10 000 контактов; если вырождено — h ≈ n.
Инвариант
Инвариант BST: для каждого узла все значения в левом поддереве меньше значения узла, а все значения в правом поддереве — больше.
Ключи должны быть сравнимы (операции </>). Правило для равных значений нужно выбрать заранее; в примере ниже дубликаты не добавляются.
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13Главное следствие: in-order обход BST даёт элементы в отсортированном порядке. Для дерева выше: 1, 3, 4, 6, 7, 8, 10, 13, 14.
Операции
Инвариант позволяет на каждом шаге отбрасывать половину оставшихся узлов — все три операции опираются на это.
Поиск: сравниваем искомое значение с текущим узлом и идём влево (если меньше) или вправо (если больше).
Вставка: ищем место как при поиске, вставляем новый узел туда, где путь заканчивается.
Удаление: зависит от количества детей у удаляемого узла.
Successor (следующий по значению) — узел с наименьшим значением в правом поддереве. Predecessor (предыдущий по значению) — узел с наибольшим значением в левом поддереве. Оба находятся за O(h): спускаемся в соответствующее поддерево и идём до упора в противоположную сторону.
Случай 1 — нет детей (лист): просто удаляем узел.
Случай 2 — один ребёнок: заменяем узел его единственным ребёнком. Инвариант BST сохраняется, потому что все значения поддерева уже были по нужную сторону от родителя.
Случай 3 — двое детей: находим successor (минимум правого поддерева), копируем его значение в удаляемый узел, затем удаляем сам successor. Successor не может иметь левого ребёнка (иначе он не минимум), поэтому его удаление сводится к случаю 1 или 2.
Удаляем 8: Successor = 10
8 10
/ \ → / \
3 14 3 14
/ /
10 13
\
13Сложность
Все три операции проходят путь от корня вниз, поэтому их сложность определяется высотой дерева.
| Операция | Лучший/средний | Худший |
|---|---|---|
| search | O(log n) | O(n) |
| insert | O(log n) | O(n) |
| delete | O(log n) | O(n) |
Худший случай — вырожденное дерево, когда все элементы выстроились в одну сторону (как связный список). Если контакты в телефонной книге добавлялись по алфавиту — Alice, Bob, Charlie, Dave, … — каждый новый элемент уходит только вправо, и дерево из 10 000 контактов превращается в цепочку высотой 10 000. Поиск деградирует до O(n) — не лучше неотсортированного массива. Для гарантированного O(log n) используют самобалансирующиеся деревья (например, AVL или red-black).
Реализация
Ruby-реализация BST без балансировки — достаточна для понимания базовых операций:
class Node
attr_accessor :value, :left, :right
def initialize(value)
@value = value
@left = nil
@right = nil
end
end
class BinarySearchTree
def initialize
@root = nil
end
def search(value)
return nil unless @root
current_node = @root
while current_node && current_node.value != value do
if value > current_node.value
current_node = current_node.right
else
current_node = current_node.left
end
end
current_node
end
def add(value)
new_node = Node.new(value)
return @root = new_node unless @root
current_node = @root
while current_node.value != value do
bigger = value > current_node.value
next_node = bigger ? current_node.right : current_node.left
unless next_node
if bigger
current_node.right = new_node
else
current_node.left = new_node
end
return new_node
end
current_node = next_node
end
current_node
end
def delete(value)
@root = delete_node(@root, value)
end
def in_order(&block)
return enum_for(:in_order) unless block_given?
return unless @root
stack = [[@root, false]]
until stack.empty? do
node, already = stack.pop
next yield node if !node.right && !node.left
if already
yield node
else
stack << [node.right, false] if node.right
stack << [node, true]
stack << [node.left, false] if node.left
end
end
self
end
private
def delete_node(node, value)
return nil unless node
if value < node.value
node.left = delete_node(node.left, value)
elsif value > node.value
node.right = delete_node(node.right, value)
else
return node.right unless node.left
return node.left unless node.right
successor = node.right
successor = successor.left while successor.left
node.value = successor.value
node.right = delete_node(node.right, successor.value)
end
node
end
endBST даёт O(log n) в среднем, но вырожденное дерево превращается в список с O(n). Для гарантированного O(log n) на произвольный поиск используют самобалансирующиеся деревья (AVL, red-black). Но если задача уже — не произвольный поиск по ключу, а только быстро добавить элемент и извлечь максимальный (или минимальный), есть более простая структура с гарантированным O(log n) и без риска вырождения: куча.
Даже сбалансированное BST хранит один ключ в узле. В оперативной памяти это не проблема, но если представить «узел = отдельное чтение с диска», высота превращается в число операций ввода-вывода. При миллионе ключей высота порядка 20 (log₂ 10⁶ ≈ 20). B-дерево решает эту проблему: один узел занимает целую страницу диска и хранит много ключей, поэтому глубина обычно получается порядка нескольких (часто 3–4).
Sources
- Cormen, Leiserson, Rivest, Stein. Introduction to Algorithms (CLRS), 4th ed. (BST invariant, search/insert/delete, высота и сложность).
Sources
- Cormen, Leiserson, Rivest, Stein. Introduction to Algorithms (CLRS), 4th ed. (BST invariant, search/insert/delete, высота и сложность).