Files
hello-algo/ru/docs/chapter_tree/binary_search_tree.md
Yudong Jin 22b3b568ef fix Ru translation (#1894)
* docs(ru): replace prose quotes with guillemets

* docs(ru): replace prose semicolons with periods

* docs(ru): align animation title forms

* docs(ru): align figure and table references
2026-04-14 18:10:12 +08:00

13 KiB
Raw Blame History

Двоичное дерево поиска

Как показано на рисунке ниже, двоичное дерево поиска (binary search tree) удовлетворяет следующим условиям.

  1. Для корневого узла все значения в левом поддереве меньше значения корневого узла, а все значения в правом поддереве больше значения корневого узла.
  2. Левое и правое поддеревья любого узла также являются двоичными деревьями поиска, то есть тоже удовлетворяют условию 1. .

Двоичное дерево поиска

Операции с двоичным деревом поиска

Мы инкапсулируем двоичное дерево поиска в класс BinarySearchTree и объявляем переменную-член root , которая указывает на корневой узел дерева.

Поиск узла

Для заданного целевого значения узла num можно выполнить поиск, опираясь на свойства двоичного дерева поиска. Как показано на рисунке ниже, мы объявляем узел cur , стартуем от корня дерева root и циклически сравниваем значения cur.val и num .

  • Если cur.val < num , это означает, что целевой узел находится в правом поддереве cur , поэтому выполняем cur = cur.right .
  • Если cur.val > num , это означает, что целевой узел находится в левом поддереве cur , поэтому выполняем cur = cur.left .
  • Если cur.val = num , это означает, что целевой узел найден, и мы выходим из цикла, возвращая этот узел.

=== "<1>" Пример поиска узла в двоичном дереве поиска

=== "<2>" bst_search_step2

=== "<3>" bst_search_step3

=== "<4>" bst_search_step4

Операция поиска в двоичном дереве поиска работает по тому же принципу, что и двоичный поиск: на каждом шаге она отбрасывает половину вариантов. Число итераций не превосходит высоты двоичного дерева, а когда дерево сбалансировано, требуется O(\log n) времени. Пример кода приведен ниже:

[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}

Вставка узла

Пусть дан элемент num , который нужно вставить. Чтобы сохранить свойство двоичного дерева поиска «левое поддерево < корень < правое поддерево», процесс вставки показан на рисунке ниже.

  1. Найти позицию для вставки: как и в операции поиска, начиная от корня, мы циклически спускаемся вниз в зависимости от соотношения между текущим значением узла и num , пока не выйдем за листовой узел (то есть не дойдем до None ).
  2. Вставить узел в найденную позицию: инициализировать узел num и поставить его на место этого None .

Вставка узла в двоичное дерево поиска

В реализации кода нужно обратить внимание на следующие два момента.

  • Двоичное дерево поиска не допускает дублирующихся узлов, иначе его определение будет нарушено. Поэтому если вставляемый узел уже существует в дереве, вставка не выполняется и функция сразу возвращается.
  • Чтобы реализовать вставку, нам нужно использовать узел pre для сохранения узла предыдущей итерации цикла. Тогда, когда обход дойдет до None , мы сможем получить его родителя и завершить вставку.
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}

Как и поиск узла, вставка узла требует O(\log n) времени.

Удаление узла

Сначала нужно найти в двоичном дереве целевой узел, а затем удалить его. Как и при вставке, после удаления необходимо сохранить свойство двоичного дерева поиска: «левое поддерево < корень < правое поддерево». Поэтому в зависимости от числа дочерних узлов у удаляемого узла, то есть для случаев со степенью 0, 1 и 2, выполняются разные операции удаления.

Как показано на рисунке ниже, когда степень удаляемого узла равна 0 , это значит, что узел является листом и может быть удален напрямую.

Удаление узла в двоичном дереве поиска (степень 0)

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

Удаление узла в двоичном дереве поиска (степень 1)

Когда степень удаляемого узла равна 2 , мы уже не можем удалить его напрямую и должны использовать для замены другой узел. Чтобы сохранить свойство двоичного дерева поиска «левое поддерево < корень < правое поддерево», этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева.

Предположим, мы выбираем минимальный узел правого поддерева, то есть следующий узел в симметричном обходе. Тогда процесс удаления показан на рисунке ниже.

  1. Найти следующий узел в «последовательности симметричного обхода» для удаляемого узла и обозначить его как tmp .
  2. Значением tmp перезаписать значение удаляемого узла, а затем рекурсивно удалить узел tmp из дерева.

=== "<1>" Удаление узла в двоичном дереве поиска (степень 2)

=== "<2>" bst_remove_case3_step2

=== "<3>" bst_remove_case3_step3

=== "<4>" bst_remove_case3_step4

Операция удаления узла также требует O(\log n) времени, где поиск удаляемого узла стоит O(\log n) , а получение следующего узла симметричного обхода также требует O(\log n) . Пример кода приведен ниже:

[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}

Упорядоченность симметричного обхода

Как показано на рисунке ниже, симметричный обход двоичного дерева следует порядку «лево \rightarrow корень \rightarrow право», а двоичное дерево поиска удовлетворяет соотношению «левый дочерний узел < корень < правый дочерний узел».

Это означает, что при симметричном обходе двоичного дерева поиска мы всегда сначала будем посещать следующий минимальный узел, и отсюда получается важное свойство: последовательность симметричного обхода двоичного дерева поиска является возрастающей.

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

Последовательность симметричного обхода двоичного дерева поиска

Эффективность двоичного дерева поиска

Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Как видно по данным в таблице ниже, временная сложность операций двоичного дерева поиска имеет логарифмический порядок и обеспечивает стабильную высокую производительность. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.

Таблица   Сравнение эффективности массива и дерева поиска

Неупорядоченный массив Двоичное дерево поиска
Поиск элемента O(n) O(\log n)
Вставка элемента O(1) O(\log n)
Удаление элемента O(n) O(\log n)

В идеальном случае двоичное дерево поиска является «сбалансированным», и тогда любой узел можно найти за \log n итераций.

Однако если в двоичное дерево поиска непрерывно вставлять и удалять узлы, оно может выродиться в связный список, как показано на рисунке ниже. Тогда временная сложность различных операций тоже вырождается до O(n) .

Деградация двоичного дерева поиска

Типичные применения двоичного дерева поиска

  • Используется как многоуровневый индекс в системах, обеспечивая эффективный поиск, вставку и удаление.
  • Служит базовой структурой данных для некоторых поисковых алгоритмов.
  • Применяется для хранения потока данных в отсортированном состоянии.