mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-04 03:34:21 +00:00
105 lines
10 KiB
Markdown
105 lines
10 KiB
Markdown
# Двоичное дерево поиска
|
||
|
||
Как показано на рисунке ниже, <u>двоичное дерево поиска (binary search tree)</u> удовлетворяет следующим условиям.
|
||
|
||
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>"
|
||

|
||
|
||
=== "<3>"
|
||

|
||
|
||
=== "<4>"
|
||

|
||
|
||
Операция поиска в двоичном дереве поиска работает по тому же принципу, что и алгоритм бинарного поиска: в каждом раунде исключается половина случаев. Количество циклов не превышает высоты двоичного дерева, когда двоичное дерево сбалансировано, используется $O(\log n)$ времени. Пример кода приведен ниже:
|
||
|
||
```src
|
||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
|
||
```
|
||
|
||
### Вставка узла
|
||
|
||
Для заданного элемента `num`, который необходимо вставить, чтобы сохранить свойство двоичного дерева поиска "левое поддерево < корневой узел < правое поддерево", процесс вставки выглядит следующим образом.
|
||
|
||
1. **Поиск позиции для вставки**: аналогично операции поиска, начиная с корневого узла, циклически выполняем поиск вниз в зависимости от соотношения между значением текущего узла и `num`, пока не выйдем за пределы листового узла (достигнем `None`), после чего выходим из цикла.
|
||
2. **Вставка узла в эту позицию**: инициализируем узел `num` и помещаем этот узел на место `None`.
|
||
|
||

|
||
|
||
В реализации кода необходимо обратить внимание на следующие два момента.
|
||
|
||
- Двоичное дерево поиска не допускает существования дублирующихся узлов, иначе это нарушит его определение. Поэтому, если узел, который необходимо вставить, уже существует в дереве, вставка не выполняется и происходит прямой возврат.
|
||
- Для реализации вставки узла нам необходимо использовать узел `pre` для сохранения узла из предыдущего раунда цикла. Таким образом, когда мы достигаем `None`, мы можем получить его родительский узел и завершить операцию вставки узла.
|
||
|
||
```src
|
||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
|
||
```
|
||
|
||
Как и при поиске узла, вставка узла использует $O(\log n)$ времени.
|
||
|
||
### Удаление узла
|
||
|
||
Сначала находим целевой узел в двоичном дереве, затем удаляем его. Аналогично вставке узла, нам необходимо обеспечить, чтобы после завершения операции удаления свойство двоичного дерева поиска "левое поддерево < корневой узел < правое поддерево" по-прежнему выполнялось. Поэтому, в зависимости от количества дочерних узлов целевого узла, мы различаем 3 случая: 0, 1 и 2, и выполняем соответствующую операцию удаления узла.
|
||
|
||
Как показано на рисунке ниже, когда степень удаляемого узла равна $0$, это означает, что узел является листовым и может быть удален напрямую.
|
||
|
||

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

|
||
|
||
Когда степень удаляемого узла равна $2$, мы не можем удалить его напрямую, а должны использовать другой узел для замены этого узла. Чтобы сохранить свойство двоичного дерева поиска "левое поддерево $<$ корневой узел $<$ правое поддерево", **этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева**.
|
||
|
||
Предположим, мы выбираем минимальный узел правого поддерева (следующий узел в симметричном обходе), тогда процесс удаления выглядит следующим образом.
|
||
|
||
1. Находим узел, следующий за удаляемым узлом в "последовательности симметричного обхода", обозначим его как `tmp`.
|
||
2. Заменяем значение удаляемого узла значением `tmp` и рекурсивно удаляем узел `tmp` в дереве.
|
||
|
||
=== "<1>"
|
||

|
||
|
||
=== "<2>"
|
||

|
||
|
||
=== "<3>"
|
||

|
||
|
||
=== "<4>"
|
||

|
||
|
||
Операция удаления узла также использует $O(\log n)$ времени, где поиск удаляемого узла требует $O(\log n)$ времени, получение узла-преемника в симметричном обходе требует $O(\log n)$ времени. Пример кода приведен ниже:
|
||
|
||
```src
|
||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
|
||
```
|
||
|
||
### Упорядоченность симметричного обхода
|
||
|
||
Как показано на рисунке ниже, симметричный обход двоичного дерева следует порядку обхода "левый $\rightarrow$ корневой $\rightarrow$ правый", а двоичное дерево поиска удовлетворяет соотношению размеров "левый дочерний узел $<$ корневой узел $<$ правый дочерний узел".
|
||
|
||
Это означает, что при выполнении симметричного обхода двоичного дерева поиска всегда сначала обходится следующий наименьший узел, что приводит к важному свойству: **последовательность симметричного обхода двоичного дерева поиска является возрастающей**.
|
||
|
||
Используя свойство возрастания симметричного обхода, мы можем получить упорядоченные данные в двоичном дереве поиска всего за $O(n)$ времени, без необходимости выполнения дополнительных операций сортировки, что очень эффективно.
|
||
|
||
 |