1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 1 из 7

Двоичное дерево поиска: порядок, встроенный в структуру

Коротко

Двоичное дерево поиска (BST) - это дерево, где у каждого узла ключ БОЛЬШЕ всех ключей в левом поддереве и МЕНЬШЕ всех в правом. Это свойство (инвариант BST) держится в КАЖДОМ узле, не только в корне. Оно превращает поиск в спуск: сравнил с узлом - и пошёл влево или вправо, отбрасывая целое поддерево. Поиск, вставка и минимум/максимум идут по одному пути от корня вниз, за время, равное ВЫСОТЕ дерева. У сбалансированного дерева высота Θ(log n) - и все операции Θ(log n). В отличие от отсортированного массива, дерево позволяет ДЁШЕВО вставлять и удалять (не сдвигая всё), сохраняя порядок. Отсюда его роль: динамическое упорядоченное множество.

Если ты знаешь инвариант BST (левое < узел < правое в каждом узле), умеешь искать и вставлять спуском от корня и понимаешь, почему цена операций - это высота дерева, листай до «Теперь сам».

Как думаешь, как искать быстро И вставлять дёшево

Отсортированный массив ищет за Θ(log n) двоичным поиском - но вставка в него Θ(n): чтобы воткнуть элемент в середину, надо сдвинуть половину. Связный список вставляет за O(1), но искать в нём Θ(n) - порядок не помогает, прыгать нельзя. Хочется забрать лучшее от обоих: быстрый поиск КАК в массиве и дешёвую вставку КАК в списке. Массив не даёт этого из-за непрерывности в памяти. А что если хранить элементы не подряд, а СВЯЗЯМИ - как список, но организованными так, чтобы можно было прыгать? Дерево поиска - ровно эта идея: порядок задаётся не адресами, а структурой ссылок, и потому и поиск дёшев, и вставка.

На что это похоже

Турнирная сетка наоборот - или, точнее, справочник-определитель растений: «цветок жёлтый? да - листай налево, нет - направо; лепестков больше пяти? …». Каждый вопрос делит оставшиеся варианты, и за десяток вопросов из тысячи видов остаётся один. Дерево поиска - такой определитель для чисел: в каждом узле вопрос «твой ключ меньше здешнего?», и ответ уводит в одну ветку, отсекая другую целиком. А добавить новый вид в определитель легко - дописать листок в нужном месте, не перетряхивая всю книгу. Это и есть дешёвая вставка.

Механизм: инвариант BST и спуск

Узел дерева хранит ключ и две ссылки - на левое и правое поддерево (любая может быть пустой). Инвариант BST: для каждого узла все ключи ЛЕВОГО поддерева меньше его ключа, все ключи ПРАВОГО - больше. Важно: это про ВСЁ поддерево, а не только про непосредственных детей.

поиск(узел, x):
    пока узел не пуст:
        если x == узел.ключ:  нашли
        если x <  узел.ключ:  узел = узел.лево      # ответ в левом поддереве
        иначе:                узел = узел.право     # в правом
    вернуть «нет»

вставка(x):
    спускаемся как в поиске, пока не упрёмся в пустую ссылку,
    и подвешиваем новый узел с ключом x на её место

Несколько слов для точности. Верхний узел - КОРЕНЬ, узлы без детей - ЛИСТЬЯ, узел вместе со всеми его потомками - ПОДДЕРЕВО. ВЫСОТА дерева - число узлов на самом длинном пути от корня до листа (у дерева из одного узла высота 1, у пустого 0).

Поиск и вставка идут по ОДНОМУ пути от корня вниз: на каждом узле одно сравнение отсекает целое поддерево. Минимум - иди всё время влево, максимум - всё время вправо. Цена любой из этих операций - длина пройденного пути, а она не больше ВЫСОТЫ дерева h.

8310 1614 всё левее 8 < 8 всё правее 8 > 8 поиск 6: 6<8 влево, 6>3 вправо - нашли за 3 шага (высота дерева)
Инвариант BST в каждом узле: левое поддерево меньше, правое больше. Поиск - спуск по одному пути; его длина не больше высоты дерева.

Правило

Двоичное дерево поиска - структура, где в КАЖДОМ узле все ключи левого поддерева меньше ключа узла, а правого - больше (инвариант BST). Поиск, вставка, минимум, максимум идут по одному пути от корня вниз, отсекая на каждом узле целое поддерево; их цена - O(h), где h - высота дерева. У сбалансированного дерева h = Θ(log n), и все операции Θ(log n). Это динамическое упорядоченное множество: и быстрый поиск (как в массиве), и дешёвые вставка с удалением (как в списке).

Разбор: почему инвариант должен держаться во ВСём поддереве, а не у детей

Тонкость, которую легко упустить. Свойство BST - «левое поддерево целиком меньше, правое целиком больше», а не «левый ребёнок меньше, правый больше». Разница критична. Возьми дерево: корень 8, правый ребёнок 10, а у 10 левый ребёнок 7. Локально всё цело: 10 > 8, 7 < 10. Но 7 оказался в ПРАВОМ поддереве восьмёрки, а 7 < 8 - инвариант BST нарушен! И поиск сломается: ища 7, мы в корне сравним 7 < 8 и уйдём ВЛЕВО, а 7 лежит справа - не найдём. Поэтому при вставке новый ключ должен попасть туда, куда приводит спуск ОТ КОРНЯ: только так он окажется в согласии со всеми предками, а не только с будущим родителем. Инвариант - глобальное свойство дерева, и держать его надо на каждом уровне спуска.

Разбор: почему обход «левый-узел-правый» даёт отсортированный порядок

У BST есть красивое следствие: если обойти его В ПОРЯДКЕ «сначала всё левое поддерево, потом сам узел, потом всё правое» (это называют СИММЕТРИЧНЫМ обходом, или inorder), ключи выйдут по ВОЗРАСТАНИЮ. Почему: по инварианту всё левое поддерево меньше узла, поэтому его надо выписать раньше узла; всё правое больше - позже. Применяя это правило рекурсивно в каждом узле, получаем полностью упорядоченную последовательность. Значит BST - это ещё и способ ХРАНИТЬ данные так, что отсортированный список достаётся одним обходом за Θ(n), без отдельной сортировки. Отсюда и практическая роль: упорядоченное множество, которое умеет и быстро искать, и в любой момент выдать всё по порядку, и при этом дёшево принимать новые элементы. (Обходы разберём отдельным уроком - там же прямой и обратный.)

Что ломается без этого

Первая ошибка - проверять инвариант только у непосредственных детей: дерево может выглядеть «локально правильным», но иметь ключ не на своём месте относительно ДАЛЬНЕГО предка - и поиск будет молча промахиваться. Вторая - вставлять узел «куда удобно», а не туда, куда приводит спуск от корня: нарушишь порядок, дерево перестанет быть BST. Третья - путать высоту с числом узлов: цена операций - это O(h), и если дерево вырождено в цепочку (урок про вырождение), h = n и всё становится O(n), а не O(log n). Четвёртая - ждать Θ(log n) от ЛЮБОГО BST: это верно только для СБАЛАНСИРОВАННОГО; обычный BST такой гарантии не даёт.

Зачем это в работе

Деревья поиска - фундамент упорядоченных структур данных. std::map и std::set в C++, TreeMap/TreeSet в Java - это сбалансированные деревья поиска (красно-чёрные, следующая глава): они держат элементы упорядоченными, дают O(log n) на поиск/вставку/удаление и позволяют то, чего не умеет хеш-таблица, - обход по порядку, поиск ближайшего, диапазонные запросы «все ключи от a до b». Индексы баз данных (B-деревья, тоже в этом разделе) - деревья поиска на диске. Понимать BST - значит понимать, ПОЧЕМУ упорядоченный контейнер медленнее хеш-таблицы в среднем (log n против 1), но даёт то, что хеш не может: порядок и гарантию худшего случая. И понимать главную опасность - вырождение, ради борьбы с которым придумана вся балансировка.

Теперь сам

В дерево поиска (изначально пустое) вставляют по очереди 5, 3, 8, 4. Нарисуй результат и скажи, за сколько сравнений найдётся 4 и почему обход «левое-узел-правое» даст 3, 4, 5, 8.

Ответ. Вставка 5 - это корень. 3 < 5 - левый ребёнок корня. 8 > 5 - правый ребёнок корня. 4: 4 < 5 влево к 3, 4 > 3 вправо - подвешиваем правым ребёнком тройки. Дерево: корень 5, слева 3 (у неё справа 4), справа 8. Поиск 4: в корне 4 < 5 идём влево к 3; 4 > 3 идём вправо - там 4, нашли. Три сравнения (5, 3, 4) - это длина пути, равная глубине узла 4. Симметричный обход: сначала левое поддерево корня (обходим 3: у неё нет левого, выписываем 3, потом её правое - 4), затем корень 5, затем правое поддерево - 8. Выходит 3, 4, 5, 8 - по возрастанию, потому что в каждом узле левое (меньшее) выписывается до узла, а правое (большее) - после.

Главное

Двоичное дерево поиска (BST) - структура, где в КАЖДОМ узле все ключи левого поддерева меньше ключа узла, а правого - больше (инвариант держится во всём поддереве, не только у детей). Поиск, вставка, min/max идут по одному пути от корня вниз, отсекая на каждом шаге целое поддерево; цена - O(h), высота дерева. У сбалансированного h = Θ(log n) → все операции Θ(log n). Симметричный обход (левое-узел-правое) выдаёт ключи по возрастанию за Θ(n). BST - динамическое упорядоченное множество: быстрый поиск плюс дешёвые вставка/удаление, чего не даёт ни массив (дорогая вставка), ни хеш (нет порядка). Главная опасность - вырождение в цепочку (h = n), с которым борется балансировка.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.