First version.

This commit is contained in:
krahets
2026-01-20 15:08:42 +08:00
parent 2213a59ff6
commit 8071daddaa
106 changed files with 11790 additions and 0 deletions
@@ -0,0 +1,68 @@
# Представление двоичного дерева с помощью массивом
При представлении в виде списка единицей хранения двоичного дерева является узел `TreeNode`, а узлы соединяются между собой указателями. В предыдущем разделе были рассмотрены основные операции с двоичным деревом, представленным в виде списка.
Можно ли представить двоичное дерево с помощью массива? Ответ положительный.
## Представление идеального двоичного дерева
Сначала рассмотрим простой пример. Если дано идеальное двоичное дерево и все его узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу соответствует уникальный индекс массива.
На основе свойств обхода по уровням можно вывести формулу отображения между индексом родительского узла и индексами дочерних узлов: **если индекс узла равен $i$, то индекс его левого дочернего узла равен $2i + 1$, а индекс правого дочернего узла равен $2i + 2$**. На рисунке ниже показаны отношения отображения между индексами различных узлов.
<!-- 🔴 俄文版缺失图片 -->
<!-- 中文原文:![完美二叉树的数组表示](array_representation_of_tree.assets/array_representation_binary_tree.png) -->
**Роль формулы отображения эквивалентна ссылкам на узлы (указателям) в списке**. Для любого узла в массиве мы можем получить доступ к его левому (правому) дочернему узлу с помощью формулы отображения.
## Представление произвольного двоичного дерева
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:完美二叉树是一个特例,在二叉树的中间层通常存在许多 `None` 。由于层序遍历序列并不包含这些 `None` ,因此我们无法仅凭该序列来推测 `None` 的数量和分布位置。**这意味着存在多种二叉树结构都符合该层序遍历序列**。 -->
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:如下图所示,给定一棵非完美二叉树,上述数组表示方法已经失效。 -->
<!-- 🔴 俄文版缺失图片 -->
<!-- 中文原文:![层序遍历序列对应多种二叉树可能性](array_representation_of_tree.assets/array_representation_without_empty.png) -->
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:为了解决此问题,**我们可以考虑在层序遍历序列中显式地写出所有 `None`** 。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。示例代码如下: -->
<!-- 🔴 俄文版缺失代码示例 -->
<!-- 中文原文:=== "Python" ... [多语言代码示例] -->
<!-- 🔴 俄文版缺失图片 -->
<!-- 中文原文:![任意类型二叉树的数组表示](array_representation_of_tree.assets/array_representation_with_empty.png) -->
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,`None` 只出现在最底层且靠右的位置,**因此所有 `None` 一定出现在层序遍历序列的末尾**。 -->
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:这意味着使用数组表示完全二叉树时,可以省略存储所有 `None` ,非常方便。下图给出了一个例子。 -->
<!-- 🔴 俄文版缺失图片 -->
<!-- 中文原文:![完全二叉树的数组表示](array_representation_of_tree.assets/array_representation_complete_binary_tree.png) -->
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:以下代码实现了一棵基于数组表示的二叉树,包括以下几种操作。 -->
<!-- 🔴 俄文版缺失列表 -->
<!-- 中文原文:- 给定某节点,获取它的值、左(右)子节点、父节点。- 获取前序遍历、中序遍历、后序遍历、层序遍历序列。 -->
<!-- 🔴 俄文版缺失代码引用 -->
<!-- 中文原文:```src [file]{array_binary_tree}-[class]{array_binary_tree}-[func]{} ``` -->
## Преимущества и ограничения
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:二叉树的数组表示主要有以下优点。 -->
<!-- 🔴 俄文版缺失列表 -->
<!-- 中文原文:- 数组存储在连续的内存空间中,对缓存友好,访问与遍历速度较快。- 不需要存储指针,比较节省空间。- 允许随机访问节点。 -->
<!-- 🔴 俄文版缺失此段落 -->
<!-- 中文原文:然而,数组表示也存在一些局限性。 -->
<!-- 🔴 俄文版缺失列表 -->
<!-- 中文原文:- 数组存储需要连续内存空间,因此不适合存储数据量过大的树。- 增删节点需要通过数组插入与删除操作实现,效率较低。- 当二叉树中存在大量 `None` 时,数组中包含的节点数据比重较低,空间利用率较低。 -->
+358
View File
@@ -0,0 +1,358 @@
# AVL-дерево *
В главе "Двоичное дерево поиска" мы упоминали, что после многократных операций вставки и удаления двоичное дерево поиска может выродиться в список. В этом случае временная сложность всех операций ухудшится с $O(\log n)$ до $O(n)$.
Как показано на рисунке ниже, после двух операций удаления узлов это двоичное дерево поиска выродится в список.
![Вырождение AVL-дерева после удаления узла](../assets/avltree_degradation_from_removing_node.png)
Например, в идеальном двоичном дереве, показанном на рисунке ниже, после вставки двух узлов дерево сильно наклонится влево, и временная сложность операции поиска также ухудшится.
![Вырождение AVL-дерева после вставки узла](../assets/avltree_degradation_from_inserting_node.png)
В 1962 году Г. М. Адельсон-Вельский и Е. М. Ландис в статье "An algorithm for the organization of information" предложили <u>AVL-дерево</u>. В статье подробно описана серия операций, обеспечивающих, что после непрерывного добавления и удаления узлов AVL-дерево не вырождается, благодаря чему временная сложность различных операций сохраняется на уровне $O(\log n)$. Другими словами, в сценариях, требующих частых операций вставки, удаления, поиска и изменения, AVL-дерево всегда может поддерживать высокую эффективность операций с данными и имеет большую практическую ценность.
## Основные термины AVL-дерева
AVL-дерево является одновременно двоичным деревом поиска и сбалансированным двоичным деревом, удовлетворяя свойствам обоих типов двоичных деревьев, поэтому является <u>сбалансированным двоичным деревом поиска (balanced binary search tree)</u>.
### Высота узла
Поскольку операции с AVL-деревом требуют получения высоты узла, необходимо добавить переменную `height` в класс узла:
=== "Python"
```python title=""
class TreeNode:
"""Класс узла AVL-дерева"""
def __init__(self, val: int):
self.val: int = val # Значение узла
self.height: int = 0 # Высота узла
self.left: TreeNode | None = None # Ссылка на левый дочерний узел
self.right: TreeNode | None = None # Ссылка на правый дочерний узел
```
=== "C++"
```cpp title=""
/* Класс узла AVL-дерева */
struct TreeNode {
int val{}; // Значение узла
int height = 0; // Высота узла
TreeNode *left{}; // Левый дочерний узел
TreeNode *right{}; // Правый дочерний узел
TreeNode() = default;
explicit TreeNode(int x) : val(x){}
};
```
=== "Java"
```java title=""
/* Класс узла AVL-дерева */
class TreeNode {
public int val; // Значение узла
public int height; // Высота узла
public TreeNode left; // Левый дочерний узел
public TreeNode right; // Правый дочерний узел
public TreeNode(int x) { val = x; }
}
```
=== "C#"
```csharp title=""
/* Класс узла AVL-дерева */
class TreeNode(int? x) {
public int? val = x; // Значение узла
public int height; // Высота узла
public TreeNode? left; // Ссылка на левый дочерний узел
public TreeNode? right; // Ссылка на правый дочерний узел
}
```
=== "Go"
```go title=""
/* Структура узла AVL-дерева */
type TreeNode struct {
Val int // Значение узла
Height int // Высота узла
Left *TreeNode // Ссылка на левый дочерний узел
Right *TreeNode // Ссылка на правый дочерний узел
}
```
=== "Swift"
```swift title=""
/* Класс узла AVL-дерева */
class TreeNode {
var val: Int // Значение узла
var height: Int // Высота узла
var left: TreeNode? // Левый дочерний узел
var right: TreeNode? // Правый дочерний узел
init(x: Int) {
val = x
height = 0
}
}
```
=== "JS"
```javascript title=""
/* Класс узла AVL-дерева */
class TreeNode {
val; // Значение узла
height; // Высота узла
left; // Указатель на левый дочерний узел
right; // Указатель на правый дочерний узел
constructor(val, left, right, height) {
this.val = val === undefined ? 0 : val;
this.height = height === undefined ? 0 : height;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
```
=== "TS"
```typescript title=""
/* Класс узла AVL-дерева */
class TreeNode {
val: number; // Значение узла
height: number; // Высота узла
left: TreeNode | null; // Указатель на левый дочерний узел
right: TreeNode | null; // Указатель на правый дочерний узел
constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val;
this.height = height === undefined ? 0 : height;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
```
=== "Dart"
```dart title=""
/* Класс узла AVL-дерева */
class TreeNode {
int val; // Значение узла
int height; // Высота узла
TreeNode? left; // Левый дочерний узел
TreeNode? right; // Правый дочерний узел
TreeNode(this.val, [this.height = 0, this.left, this.right]);
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* Структура узла AVL-дерева */
struct TreeNode {
val: i32, // Значение узла
height: i32, // Высота узла
left: Option<Rc<RefCell<TreeNode>>>, // Левый дочерний узел
right: Option<Rc<RefCell<TreeNode>>>, // Правый дочерний узел
}
impl TreeNode {
/* Конструктор */
fn new(val: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
val,
height: 0,
left: None,
right: None
}))
}
}
```
=== "C"
```c title=""
/* Структура узла AVL-дерева */
typedef struct TreeNode {
int val;
int height;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
/* Конструктор */
TreeNode *newTreeNode(int val) {
TreeNode *node;
node = (TreeNode *)malloc(sizeof(TreeNode));
node->val = val;
node->height = 0;
node->left = NULL;
node->right = NULL;
return node;
}
```
=== "Kotlin"
```kotlin title=""
/* Класс узла AVL-дерева */
class TreeNode(val _val: Int) { // Значение узла
val height: Int = 0 // Высота узла
val left: TreeNode? = null // Левый дочерний узел
val right: TreeNode? = null // Правый дочерний узел
}
```
=== "Ruby"
```ruby title=""
### Класс узла AVL-дерева ###
class TreeNode
attr_accessor :val # Значение узла
attr_accessor :height # Высота узла
attr_accessor :left # Ссылка на левый дочерний узел
attr_accessor :right # Ссылка на правый дочерний узел
def initialize(val)
@val = val
@height = 0
end
end
```
"Высота узла" — это расстояние от данного узла до самого удаленного листового узла, т. е. количество пройденных "ребер". Особо следует отметить, что высота листового узла равна $0$, а высота пустого узла равна $-1$. Мы создадим две вспомогательные функции для получения и обновления высоты узла:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{update_height}
```
### Коэффициент балансировки узла
<u>Коэффициент балансировки (balance factor)</u> узла определяется как высота левого поддерева минус высота правого поддерева, при этом коэффициент балансировки пустого узла равен $0$. Мы также инкапсулируем функцию получения коэффициента балансировки узла для удобства последующего использования:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{balance_factor}
```
!!! tip "Подсказка"
Пусть коэффициент балансировки равен $f$, тогда коэффициент балансировки любого узла AVL-дерева удовлетворяет условию $-1 \le f \le 1$.
## Повороты AVL-дерева
Особенность AVL-дерева заключается в операции "поворота", которая позволяет восстановить баланс несбалансированного узла без нарушения порядка обхода двоичного дерева в симметричном порядке. Другими словами, **операция поворота сохраняет свойство "двоичного дерева поиска" и делает дерево снова "сбалансированным двоичным деревом"**.
Узел с абсолютным значением коэффициента балансировки $> 1$ называется "несбалансированным узлом". В зависимости от ситуации несбалансированности узла операции поворота делятся на четыре типа: правый поворот, левый поворот, сначала правый поворот затем левый поворот, сначала левый поворот затем правый поворот. Ниже подробно описаны эти операции поворота.
### Правый поворот
Как показано на рисунке ниже, под узлом указан коэффициент балансировки. Снизу вверх первый несбалансированный узел в двоичном дереве — это "узел 3". Рассмотрим поддерево с этим несбалансированным узлом в качестве корневого узла, обозначим этот узел как `node`, его левый дочерний узел как `child`, и выполним "правый поворот". После завершения правого поворота поддерево восстанавливает баланс и по-прежнему сохраняет свойство двоичного дерева поиска.
=== "<1>"
![Шаги операции правого поворота](../assets/avltree_right_rotate_step1.png)
=== "<2>"
![avltree_right_rotate_step2](../assets/avltree_right_rotate_step2.png)
=== "<3>"
![avltree_right_rotate_step3](../assets/avltree_right_rotate_step3.png)
=== "<4>"
![avltree_right_rotate_step4](../assets/avltree_right_rotate_step4.png)
Как показано на рисунке ниже, когда узел `child` имеет правый дочерний узел (обозначим его как `grand_child`), необходимо добавить один шаг в правый поворот: сделать `grand_child` левым дочерним узлом `node`.
![Операция правого поворота с grand_child](../assets/avltree_right_rotate_with_grandchild.png)
"Поворот вправо" — это образное выражение, на самом деле это реализуется путем изменения указателей узлов, код показан ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{right_rotate}
```
### Левый поворот
Соответственно, если рассмотреть "зеркальное отражение" вышеупомянутого несбалансированного двоичного дерева, необходимо выполнить "левый поворот", показанный на рисунке ниже.
![Операция левого поворота](../assets/avltree_left_rotate.png)
Аналогично, как показано на рисунке ниже, когда узел `child` имеет левый дочерний узел (обозначим его как `grand_child`), необходимо добавить один шаг в левый поворот: сделать `grand_child` правым дочерним узлом `node`.
![Операция левого поворота с grand_child](../assets/avltree_left_rotate_with_grandchild.png)
Можно заметить, что **операции правого и левого поворотов логически зеркально симметричны, и две ситуации несбалансированности, которые они решают, также симметричны**. Основываясь на симметрии, нам нужно только заменить все `left` на `right` и все `right` на `left` в коде реализации правого поворота, чтобы получить код реализации левого поворота:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{left_rotate}
```
### Сначала левый поворот, затем правый поворот
Для несбалансированного узла 3 на рисунке ниже использование только левого или правого поворота не может восстановить баланс поддерева. В этом случае необходимо сначала выполнить "левый поворот" для `child`, а затем "правый поворот" для `node`.
![Сначала левый поворот, затем правый поворот](../assets/avltree_left_right_rotate.png)
### Сначала правый поворот, затем левый поворот
Как показано на рисунке ниже, для зеркальной ситуации вышеупомянутого несбалансированного двоичного дерева необходимо сначала выполнить "правый поворот" для `child`, а затем "левый поворот" для `node`.
![Сначала правый поворот, затем левый поворот](../assets/avltree_right_left_rotate.png)
### Выбор поворота
На рисунке ниже показаны четыре ситуации несбалансированности, соответствующие вышеупомянутым случаям, для которых требуются операции правого поворота, сначала левого поворота затем правого поворота, сначала правого поворота затем левого поворота и левого поворота соответственно.
![Четыре ситуации поворота AVL-дерева](../assets/avltree_rotation_cases.png)
Как показано в таблице ниже, мы определяем, к какой ситуации на рисунке выше относится несбалансированный узел, путем оценки знака коэффициента балансировки несбалансированного узла и коэффициента балансировки дочернего узла с большей высотой.
<p align="center"> Таблица <id> &nbsp; Условия выбора четырех типов поворотов </p>
| Коэффициент балансировки несбалансированного узла | Коэффициент балансировки дочернего узла | Применяемый метод поворота |
| ------------------ | ---------------- | ---------------- |
| $> 1$ (левое смещение) | $\geq 0$ | Правый поворот |
| $> 1$ (левое смещение) | $<0$ | Сначала левый поворот, затем правый поворот |
| $< -1$ (правое смещение) | $\leq 0$ | Левый поворот |
| $< -1$ (правое смещение) | $>0$ | Сначала правый поворот, затем левый поворот |
Для удобства использования мы инкапсулируем операцию поворота в функцию. **С помощью этой функции мы можем выполнять повороты для различных ситуаций несбалансированности, восстанавливая баланс несбалансированных узлов**. Код показан ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{rotate}
```
## Основные операции с AVL-деревом
### Вставка узла
Операция вставки узла в AVL-дерево в основном аналогична операции в двоичном дереве поиска. Единственное отличие заключается в том, что после вставки узла в AVL-дерево на пути от этого узла к корневому узлу может появиться серия несбалансированных узлов. Поэтому **необходимо начиная с этого узла снизу вверх выполнять операции поворота, чтобы восстановить баланс всех несбалансированных узлов**. Код показан ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{insert_helper}
```
### Удаление узла
Аналогично, на основе метода удаления узла в двоичном дереве поиска необходимо снизу вверх выполнять операции поворота, чтобы восстановить баланс всех несбалансированных узлов. Код показан ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{remove_helper}
```
### Поиск узла
Операция поиска узла в AVL-дереве идентична операции в двоичном дереве поиска, здесь не будем повторяться.
## Типичные применения AVL-дерева
- Организация и хранение больших объемов данных, подходит для сценариев с высокой частотой поиска и низкой частотой вставки и удаления.
- Используется для построения системы индексов в базах данных.
- Красно-черное дерево также является распространенным сбалансированным двоичным деревом поиска. По сравнению с AVL-деревом условия балансировки красно-черного дерева более мягкие, для операций вставки и удаления узлов требуется меньше операций поворота, средняя эффективность операций добавления и удаления узлов выше.
+105
View File
@@ -0,0 +1,105 @@
# Двоичное дерево поиска
Как показано на рисунке ниже, <u>двоичное дерево поиска (binary search tree)</u> удовлетворяет следующим условиям.
1. Для корневого узла значения всех узлов в левом поддереве $<$ значения корневого узла $<$ значения всех узлов в правом поддереве.
2. Левое и правое поддеревья любого узла также являются двоичными деревьями поиска, т. е. также удовлетворяют условию `1.`.
![Двоичное дерево поиска](../assets/binary_search_tree.png)
## Операции с двоичным деревом поиска
Мы инкапсулируем двоичное дерево поиска в класс `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>"
![Пример поиска узла в двоичном дереве поиска](../assets/bst_search_step1.png)
=== "<2>"
![bst_search_step2](../assets/bst_search_step2.png)
=== "<3>"
![bst_search_step3](../assets/bst_search_step3.png)
=== "<4>"
![bst_search_step4](../assets/bst_search_step4.png)
Операция поиска в двоичном дереве поиска работает по тому же принципу, что и алгоритм бинарного поиска: в каждом раунде исключается половина случаев. Количество циклов не превышает высоты двоичного дерева, когда двоичное дерево сбалансировано, используется $O(\log n)$ времени. Пример кода приведен ниже:
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
```
### Вставка узла
Для заданного элемента `num`, который необходимо вставить, чтобы сохранить свойство двоичного дерева поиска "левое поддерево < корневой узел < правое поддерево", процесс вставки выглядит следующим образом.
1. **Поиск позиции для вставки**: аналогично операции поиска, начиная с корневого узла, циклически выполняем поиск вниз в зависимости от соотношения между значением текущего узла и `num`, пока не выйдем за пределы листового узла (достигнем `None`), после чего выходим из цикла.
2. **Вставка узла в эту позицию**: инициализируем узел `num` и помещаем этот узел на место `None`.
![Вставка узла в двоичное дерево поиска](../assets/bst_insert.png)
В реализации кода необходимо обратить внимание на следующие два момента.
- Двоичное дерево поиска не допускает существования дублирующихся узлов, иначе это нарушит его определение. Поэтому, если узел, который необходимо вставить, уже существует в дереве, вставка не выполняется и происходит прямой возврат.
- Для реализации вставки узла нам необходимо использовать узел `pre` для сохранения узла из предыдущего раунда цикла. Таким образом, когда мы достигаем `None`, мы можем получить его родительский узел и завершить операцию вставки узла.
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
```
Как и при поиске узла, вставка узла использует $O(\log n)$ времени.
### Удаление узла
Сначала находим целевой узел в двоичном дереве, затем удаляем его. Аналогично вставке узла, нам необходимо обеспечить, чтобы после завершения операции удаления свойство двоичного дерева поиска "левое поддерево < корневой узел < правое поддерево" по-прежнему выполнялось. Поэтому, в зависимости от количества дочерних узлов целевого узла, мы различаем 3 случая: 0, 1 и 2, и выполняем соответствующую операцию удаления узла.
Как показано на рисунке ниже, когда степень удаляемого узла равна $0$, это означает, что узел является листовым и может быть удален напрямую.
![Удаление узла в двоичном дереве поиска (степень 0)](../assets/bst_remove_case1.png)
Как показано на рисунке ниже, когда степень удаляемого узла равна $1$, достаточно заменить удаляемый узел его дочерним узлом.
![Удаление узла в двоичном дереве поиска (степень 1)](../assets/bst_remove_case2.png)
Когда степень удаляемого узла равна $2$, мы не можем удалить его напрямую, а должны использовать другой узел для замены этого узла. Чтобы сохранить свойство двоичного дерева поиска "левое поддерево $<$ корневой узел $<$ правое поддерево", **этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева**.
Предположим, мы выбираем минимальный узел правого поддерева (следующий узел в симметричном обходе), тогда процесс удаления выглядит следующим образом.
1. Находим узел, следующий за удаляемым узлом в "последовательности симметричного обхода", обозначим его как `tmp`.
2. Заменяем значение удаляемого узла значением `tmp` и рекурсивно удаляем узел `tmp` в дереве.
=== "<1>"
![Удаление узла в двоичном дереве поиска (степень 2)](../assets/bst_remove_case3_step1.png)
=== "<2>"
![bst_remove_case3_step2](../assets/bst_remove_case3_step2.png)
=== "<3>"
![bst_remove_case3_step3](../assets/bst_remove_case3_step3.png)
=== "<4>"
![bst_remove_case3_step4](../assets/bst_remove_case3_step4.png)
Операция удаления узла также использует $O(\log n)$ времени, где поиск удаляемого узла требует $O(\log n)$ времени, получение узла-преемника в симметричном обходе требует $O(\log n)$ времени. Пример кода приведен ниже:
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
```
### Упорядоченность симметричного обхода
Как показано на рисунке ниже, симметричный обход двоичного дерева следует порядку обхода "левый $\rightarrow$ корневой $\rightarrow$ правый", а двоичное дерево поиска удовлетворяет соотношению размеров "левый дочерний узел $<$ корневой узел $<$ правый дочерний узел".
Это означает, что при выполнении симметричного обхода двоичного дерева поиска всегда сначала обходится следующий наименьший узел, что приводит к важному свойству: **последовательность симметричного обхода двоичного дерева поиска является возрастающей**.
Используя свойство возрастания симметричного обхода, мы можем получить упорядоченные данные в двоичном дереве поиска всего за $O(n)$ времени, без необходимости выполнения дополнительных операций сортировки, что очень эффективно.
![Последовательность симметричного обхода двоичного дерева поиска](../assets/bst_inorder_traversal.png)
+620
View File
@@ -0,0 +1,620 @@
# Двоичные деревья
<u>Двоичное (бинарное) дерево (binary tree)</u> -- это нелинейная структура данных, представляющая отношения между предками и потомками и отражающая логику «разделяй и властвуй». Подобно спискам, основным элементом двоичного дерева является узел, который содержит значение, ссылку на левый дочерний узел и ссылку на правый дочерний узел.
=== "Python"
```python title=""
class TreeNode:
"""Класс узла двоичного дерева"""
def __init__(self, val: int):
self.val: int = val # Значение узла
self.left: TreeNode | None = None # Ссылка на левый дочерний узел
self.right: TreeNode | None = None # Ссылка на правый дочерний узел
```
=== "C++"
```cpp title=""
/* Структура узла двоичного дерева */
struct TreeNode {
int val; // Значение узла
TreeNode *left; // Указатель на левый дочерний узел
TreeNode *right; // Указатель на правый дочерний узел
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
```
=== "Java"
```java title=""
/* Класс узла двоичного дерева */
class TreeNode {
int val; // Значение узла
TreeNode left; // Ссылка на левый дочерний узел
TreeNode right; // Ссылка на правый дочерний узел
TreeNode(int x) { val = x; }
}
```
=== "C#"
```csharp title=""
/* Класс узла двоичного дерева */
class TreeNode(int? x) {
public int? val = x; // Значение узла
public TreeNode? left; // Ссылка на левый дочерний узел
public TreeNode? right; // Ссылка на правый дочерний узел
}
```
=== "Go"
```go title=""
/* Структура узла двоичного дерева */
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
/* Конструктор */
func NewTreeNode(v int) *TreeNode {
return &TreeNode{
Left: nil, // Указатель на левый дочерний узел
Right: nil, // Указатель на правый дочерний узел
Val: v, // Значение узла
}
}
```
=== "Swift"
```swift title=""
/* Класс узла двоичного дерева */
class TreeNode {
var val: Int // Значение узла
var left: TreeNode? // Ссылка на левый дочерний узел
var right: TreeNode? // Ссылка на правый дочерний узел
init(x: Int) {
val = x
}
}
```
=== "JS"
```javascript title=""
/* Класс узла двоичного дерева */
class TreeNode {
val; // Значение узла
left; // Указатель на левый дочерний узел
right; // Указатель на правый дочерний узел
constructor(val, left, right) {
this.val = val === undefined ? 0 : val;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
}
```
=== "TS"
```typescript title=""
/* Класс узла двоичного дерева */
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val; // Значение узла
this.left = left === undefined ? null : left; // Ссылка на левый дочерний узел
this.right = right === undefined ? null : right; // Ссылка на правый дочерний узел
}
}
```
=== "Dart"
```dart title=""
/* Класс узла двоичного дерева */
class TreeNode {
int val; // Значение узла
TreeNode? left; // Ссылка на левый дочерний узел
TreeNode? right; // Ссылка на правый дочерний узел
TreeNode(this.val, [this.left, this.right]);
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* Структура узла двоичного дерева */
struct TreeNode {
val: i32, // Значение узла
left: Option<Rc<RefCell<TreeNode>>>, // Ссылка на левый дочерний узел
right: Option<Rc<RefCell<TreeNode>>>, // Ссылка на правый дочерний узел
}
impl TreeNode {
/* Конструктор */
fn new(val: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
val,
left: None,
right: None
}))
}
}
```
=== "C"
```c title=""
/* Структура узла двоичного дерева */
typedef struct TreeNode {
int val; // Значение узла
int height; // Высота узла
struct TreeNode *left; // Указатель на левый дочерний узел
struct TreeNode *right; // Указатель на правый дочерний узел
} TreeNode;
/* Функция-конструктор */
TreeNode *newTreeNode(int val) {
TreeNode *node;
node = (TreeNode *)malloc(sizeof(TreeNode));
node->val = val;
node->height = 0;
node->left = NULL;
node->right = NULL;
return node;
}
```
=== "Kotlin"
```kotlin title=""
/* Класс узла двоичного дерева */
class TreeNode(val _val: Int) { // Значение узла
val left: TreeNode? = null // Ссылка на левый дочерний узел
val right: TreeNode? = null // Ссылка на правый дочерний узел
}
```
=== "Ruby"
```ruby title=""
### Класс узла двоичного дерева ###
class TreeNode
attr_accessor :val # Значение узла
attr_accessor :left # Ссылка на левый дочерний узел
attr_accessor :right # Ссылка на правый дочерний узел
def initialize(val)
@val = val
end
end
```
Каждый узел имеет две ссылки (указателя), указывающие на <u>левый дочерний узел (left-child node)</u> и <u>правый дочерний узел (right-child node)</u>. Текущий узел называется <u>родительским узлом (parent node)</u> для этих двух дочерних узлов. Для заданного узла дерево, образованное его левым дочерним узлом и всеми его подузлами, называется <u>левым поддеревом (left subtree)</u>. Аналогично определяется <u>правое поддерево (right subtree)</u>.
**В двоичном дереве, кроме листовых узлов, все остальные узлы содержат дочерние узлы и непустые поддеревья**. Как показано на рисунке ниже, если рассматривать «узел 2» как родительский, то его левым и правым дочерними узлами будут «узел 4» и «узел 5» соответственно. Левое поддерево -- это «узел 4 и все узлы ниже него», а правое поддерево -- «узел 5 и все узлы ниже него».
![Родительский узел, дочерние узлы, поддеревья](../assets/binary_tree_definition.png)
## Основные понятия двоичного дерева
Основные понятия двоичного дерева показаны на рисунке ниже.
- <u>Корневой узел (root node)</u>: узел, находящийся на верхнем уровне дерева и не имеющий родительского узла.
- <u>Листовой узел (leaf node)</u>: узел, не имеющий дочерних узлов, оба его указателя указывают на `None`.
- <u>Ребро (edge)</u>: отрезок, соединяющий два узла, т. е. ссылка (указатель) узла.
- <u>Уровень узла (level)</u>: увеличивается сверху вниз, уровень корневого узла равен 1.
- <u>Степень узла (degree)</u>: количество дочерних узлов узла. В двоичном дереве степень может быть 0, 1 или 2.
- <u>Высота двоичного дерева (height)</u>: количество ребер от корневого узла до самого удаленного листового узла.
- <u>Глубина узла (depth)</u>: количество ребер от корневого узла до данного узла.
- <u>Высота узла (height)</u>: количество ребер от самого удаленного листового узла до данного узла.
![Основные понятия двоичного дерева](../assets/binary_tree_terminology.png)
!!! tip
Обратите внимание, что обычно мы определяем «высоту» и «глубину» как «количество пройденных ребер», но в некоторых задачах или учебниках они могут определяться как «количество пройденных узлов». В этом случае высота и глубина должны быть увеличены на 1.
## Основные операции с двоичными деревьями
### Инициализация двоичного дерева
Подобно спискам, сначала инициализируются узлы, затем строятся ссылки (указатели).
=== "Python"
```python title="binary_tree.py"
# Инициализация двоичного дерева
# Инициализация узлов
n1 = TreeNode(val=1)
n2 = TreeNode(val=2)
n3 = TreeNode(val=3)
n4 = TreeNode(val=4)
n5 = TreeNode(val=5)
# Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "C++"
```cpp title="binary_tree.cpp"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode* n1 = new TreeNode(1);
TreeNode* n2 = new TreeNode(2);
TreeNode* n3 = new TreeNode(3);
TreeNode* n4 = new TreeNode(4);
TreeNode* n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
```
=== "Java"
```java title="binary_tree.java"
// Инициализация узлов
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "C#"
```csharp title="binary_tree.cs"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode n1 = new(1);
TreeNode n2 = new(2);
TreeNode n3 = new(3);
TreeNode n4 = new(4);
TreeNode n5 = new(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Go"
```go title="binary_tree.go"
/* Инициализация двоичного дерева */
// Инициализация узлов
n1 := NewTreeNode(1)
n2 := NewTreeNode(2)
n3 := NewTreeNode(3)
n4 := NewTreeNode(4)
n5 := NewTreeNode(5)
// Построение ссылок (указателей) между узлами
n1.Left = n2
n1.Right = n3
n2.Left = n4
n2.Right = n5
```
=== "Swift"
```swift title="binary_tree.swift"
// Инициализация узлов
let n1 = TreeNode(x: 1)
let n2 = TreeNode(x: 2)
let n3 = TreeNode(x: 3)
let n4 = TreeNode(x: 4)
let n5 = TreeNode(x: 5)
// Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "JS"
```javascript title="binary_tree.js"
/* Инициализация двоичного дерева */
// Инициализация узлов
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "TS"
```typescript title="binary_tree.ts"
/* Инициализация двоичного дерева */
// Инициализация узлов
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Dart"
```dart title="binary_tree.dart"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Rust"
```rust title="binary_tree.rs"
// Инициализация узлов
let n1 = TreeNode::new(1);
let n2 = TreeNode::new(2);
let n3 = TreeNode::new(3);
let n4 = TreeNode::new(4);
let n5 = TreeNode::new(5);
// Построение ссылок (указателей) между узлами
n1.borrow_mut().left = Some(n2.clone());
n1.borrow_mut().right = Some(n3);
n2.borrow_mut().left = Some(n4);
n2.borrow_mut().right = Some(n5);
```
=== "C"
```c title="binary_tree.c"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode *n1 = newTreeNode(1);
TreeNode *n2 = newTreeNode(2);
TreeNode *n3 = newTreeNode(3);
TreeNode *n4 = newTreeNode(4);
TreeNode *n5 = newTreeNode(5);
// Построение ссылок (указателей) между узлами
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
```
=== "Kotlin"
```kotlin title="binary_tree.kt"
// Инициализация узлов
val n1 = TreeNode(1)
val n2 = TreeNode(2)
val n3 = TreeNode(3)
val n4 = TreeNode(4)
val n5 = TreeNode(5)
// Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "Ruby"
```ruby title="binary_tree.rb"
# Инициализация двоичного дерева
# Инициализация узлов
n1 = TreeNode.new(1)
n2 = TreeNode.new(2)
n3 = TreeNode.new(3)
n4 = TreeNode.new(4)
n5 = TreeNode.new(5)
# Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
??? pythontutor "Визуализация выполнения"
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
### Вставка и удаление узлов
Подобно спискам, в двоичном дереве вставку и удаление узлов можно выполнять путем изменения указателей. На рисунке ниже приведен пример.
![Вставка и удаление узлов в двоичном дереве](../assets/binary_tree_add_remove.png)
=== "Python"
```python title="binary_tree.py"
# Вставка и удаление узлов
p = TreeNode(0)
# Вставка узла P между n1 и n2
n1.left = p
p.left = n2
# Удаление узла P
n1.left = n2
```
=== "C++"
```cpp title="binary_tree.cpp"
/* Вставка и удаление узлов */
TreeNode* P = new TreeNode(0);
// Вставка узла P между n1 и n2
n1->left = P;
P->left = n2;
// Удаление узла P
n1->left = n2;
// Освобождение памяти
delete P;
```
=== "Java"
```java title="binary_tree.java"
TreeNode P = new TreeNode(0);
// Вставка узла P между n1 и n2
n1.left = P;
P.left = n2;
// Удаление узла P
n1.left = n2;
```
=== "C#"
```csharp title="binary_tree.cs"
/* Вставка и удаление узлов */
TreeNode P = new(0);
// Вставка узла P между n1 и n2
n1.left = P;
P.left = n2;
// Удаление узла P
n1.left = n2;
```
=== "Go"
```go title="binary_tree.go"
/* Вставка и удаление узлов */
// Вставка узла P между n1 и n2
p := NewTreeNode(0)
n1.Left = p
p.Left = n2
// Удаление узла P
n1.Left = n2
```
=== "Swift"
```swift title="binary_tree.swift"
let P = TreeNode(x: 0)
// Вставка узла P между n1 и n2
n1.left = P
P.left = n2
// Удаление узла P
n1.left = n2
```
=== "JS"
```javascript title="binary_tree.js"
/* Вставка и удаление узлов */
let P = new TreeNode(0);
// Вставка узла P между n1 и n2
n1.left = P;
P.left = n2;
// Удаление узла P
n1.left = n2;
```
=== "TS"
```typescript title="binary_tree.ts"
/* Вставка и удаление узлов */
const P = new TreeNode(0);
// Вставка узла P между n1 и n2
n1.left = P;
P.left = n2;
// Удаление узла P
n1.left = n2;
```
=== "Dart"
```dart title="binary_tree.dart"
/* Вставка и удаление узлов */
TreeNode P = new TreeNode(0);
// Вставка узла P между n1 и n2
n1.left = P;
P.left = n2;
// Удаление узла P
n1.left = n2;
```
=== "Rust"
```rust title="binary_tree.rs"
let p = TreeNode::new(0);
// Вставка узла P между n1 и n2
n1.borrow_mut().left = Some(p.clone());
p.borrow_mut().left = Some(n2.clone());
// Удаление узла p
n1.borrow_mut().left = Some(n2);
```
=== "C"
```c title="binary_tree.c"
/* Вставка и удаление узлов */
TreeNode *P = newTreeNode(0);
// Вставка узла P между n1 и n2
n1->left = P;
P->left = n2;
// Удаление узла P
n1->left = n2;
// Освобождение памяти
free(P);
```
=== "Kotlin"
```kotlin title="binary_tree.kt"
val P = TreeNode(0)
// Вставка узла P между n1 и n2
n1.left = P
P.left = n2
// Удаление узла P
n1.left = n2
```
=== "Ruby"
```ruby title="binary_tree.rb"
# Вставка и удаление узлов
_p = TreeNode.new(0)
# Вставка узла _p между n1 и n2
n1.left = _p
_p.left = n2
# Удаление узла
n1.left = n2
```
??? pythontutor "Визуализация выполнения"
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%
@@ -0,0 +1,88 @@
# Обход двоичного дерева
С физической точки зрения дерево является структурой данных, основанной на связном списке, поэтому его обход осуществляется последовательным доступом к узлам через указатели. Однако, будучи нелинейной структурой данных, обход дерева сложнее, чем обход связного списка, и требует использования алгоритмов поиска.
Наиболее распространенные методы обхода двоичного дерева включают обход по уровням, прямой, симметричный и обратный обходы.
## Обход по уровням
Обход по уровням осуществляется сверху вниз, выполняется последовательный обход двоичного дерева с посещением узлов на каждом уровне слева направо, как показано на рис. 7.9.
Обход по уровням по своей сути является обходом в ширину, также называемым поиском в ширину, который характеризуется постепенно расширяющимся кольцом от центра к периферии.
![Обход двоичного дерева по уровням](../assets/media/image228.jpeg)
### Код реализации
Обход в ширину обычно реализуется с использованием очереди. Очередь следует принципу «первый вошел -- первый вышел», а обход в ширину -- принципу «поэтапное продвижение», что делает их концептуально схожими. Ниже приведен код реализации.
```src
[file]{binary_tree_bfs}-[class]{}-[func]{level_order}
```
### Анализ сложности
**Временная сложность** *O*(*n*): каждый узел посещается один раз, что занимает *O*(*n*) времени выполнения, где *n* -- количество узлов.
**Пространственная сложность** *O*(*n*): в худшем случае, т. е. в полном двоичном дереве, до достижения самого нижнего уровня в очереди может находиться одновременно (*n* + 1)/2 узлов, что занимает *O*(*n*) пространства.
## Прямой, симметричный и обратный обходы
Прямой, симметричный и обратный обходы относятся к обходам в глубину, также называемым поиск в глубину, который характеризуется подходом «сначала до конца, затем возврат и продолжение».
На рис. 7.10 демонстрируется принцип работы обхода в глубину для двоичного дерева. **Обход в глубину можно представить как обход двоичного дерева по периметру**, при этом на каждом узле встречаются три позиции, соответствующие прямому, симметричному и обратному обходам.
![Прямой, симметричный и обратный обходы двоичного дерева](../assets/media/image237.jpeg)
### Код реализации
Поиск в глубину обычно реализуется на основе рекурсии.
```src
[file]{binary_tree_dfs}-[class]{}-[func]{post_order}
```
На рис. 7.11 демонстрируется рекурсивный процесс прямого обхода двоичного дерева, который можно разделить на два противоположных этапа: рекурсия и возврат.
1. Рекурсия означает начало нового метода, в процессе которого программа посещает следующий узел.
2. Возврат означает возвращение функции, что указывает на завершение посещения текущего узла.
=== "<1>"
![Рекурсивный процесс прямого обхода](../assets/media/image246.jpeg)
=== "<2>"
![preorder_step2](../assets/media/image249.jpeg)
=== "<3>"
![preorder_step3](../assets/media/image251.jpeg)
=== "<4>"
![preorder_step4](../assets/media/image253.jpeg)
=== "<5>"
![preorder_step5](../assets/media/image256.jpeg)
=== "<6>"
![preorder_step6](../assets/media/image259.jpeg)
=== "<7>"
![preorder_step7](../assets/media/image261.jpeg)
=== "<8>"
![preorder_step8](../assets/media/image263.jpeg)
=== "<9>"
![preorder_step9](../assets/media/image266.jpeg)
=== "<10>"
![preorder_step10](../assets/media/image269.jpeg)
=== "<11>"
![preorder_step11](../assets/media/image271.jpeg)
### Анализ сложности
**Временная сложность** *O*(*n*): все узлы посещаются один раз, что занимает *O*(*n*) времени.
**Пространственная сложность** *O*(*n*): в худшем случае, когда дерево вырождается в список, глубина рекурсии достигает *n*, система занимает *O*(*n*) пространства стека.
+10
View File
@@ -0,0 +1,10 @@
```markdown
# Деревья
![](../assets/media/image210.jpeg)
!!! abstract
Двоичное (бинарное) дерево -- это нелинейная структура данных, представляющая отношения между предками и потомками и отражающая логику «разделяй и властвуй».
```
+54
View File
@@ -0,0 +1,54 @@
# Резюме
### Основные моменты
- Двоичное дерево — это нелинейная структура данных, отражающая логику «разделяй и властвуй». Каждый узел двоичного дерева содержит значение и два указателя, указывающие на его левый и правый дочерние узлы.
- Для заданного узла дерева дерево, образованное его левым (правым) дочерним узлом и всеми узлами ниже него, называется левым (правым) поддеревом этого узла.
- Основные понятия двоичного дерева включают корневой узел, листовой узел, уровень, степень, ребро, высоту и глубину.
- Операции инициализации двоичного дерева, вставки и удаления узлов аналогичны операциям со связными списками.
- Распространенные типы двоичных деревьев включают идеальное двоичное дерево, совершенное двоичное дерево, полное двоичное дерево и сбалансированное двоичное дерево. Идеальное двоичное дерево является наиболее оптимальным состоянием, а связный список — наихудшим вырожденным состоянием.
- Двоичное дерево может быть представлено с помощью массива, при этом значения узлов и пустые позиции располагаются в порядке обхода по уровням, а указатели реализуются на основе отношений индексов между родительскими и дочерними узлами.
- Обход двоичного дерева по уровням является методом поиска в ширину, который отражает способ обхода «постепенное расширение кольцами», обычно реализуемый с помощью очереди.
- Прямой, симметричный и обратный обходы относятся к поиску в глубину, который отражает способ обхода «сначала до конца, затем возврат и продолжение», обычно реализуемый с помощью рекурсии.
- Двоичное дерево поиска — это эффективная структура данных для поиска элементов, временная сложность операций поиска, вставки и удаления составляет $O(\log n)$. Когда двоичное дерево поиска вырождается в связный список, временная сложность всех операций ухудшается до $O(n)$.
- AVL-дерево, также называемое сбалансированным двоичным деревом поиска, обеспечивает сохранение баланса дерева после непрерывных операций вставки и удаления узлов с помощью операций поворота.
- Операции поворота AVL-дерева включают правый поворот, левый поворот, сначала правый затем левый поворот, сначала левый затем правый поворот. После вставки или удаления узла AVL-дерево выполняет операции поворота снизу вверх, чтобы восстановить баланс дерева.
### Вопросы и ответы
**В**: Для двоичного дерева с одним узлом высота дерева и глубина корневого узла равны $0$?
Да, поскольку высота и глубина обычно определяются как «количество пройденных ребер».
**В**: Вставка и удаление в двоичном дереве обычно выполняются набором операций. Что означает «набор операций»? Можно ли понимать это как освобождение ресурсов дочерних узлов?
Возьмем в качестве примера двоичное дерево поиска: операция удаления узла требует обработки трех различных случаев, и в каждом случае необходимо выполнить несколько шагов операций с узлами.
**В**: Почему обход DFS двоичного дерева имеет три порядка: прямой, симметричный и обратный, и для чего они используются?
Подобно прямому и обратному обходу массива, прямой, симметричный и обратный обходы — это три метода обхода двоичного дерева, с помощью которых мы можем получить результат обхода в определенном порядке. Например, в двоичном дереве поиска, поскольку размер узлов удовлетворяет условию `значение левого дочернего узла < значение корневого узла < значение правого дочернего узла`, если мы обходим дерево в приоритете «левый $\rightarrow$ корень $\rightarrow$ правый», мы можем получить упорядоченную последовательность узлов.
**В**: Операция правого поворота обрабатывает отношения между несбалансированными узлами `node`, `child`, `grand_child`. Нужно ли поддерживать связь между родительским узлом `node` и самим `node`? Не разорвется ли она после операции правого поворота?
Нужно рассматривать эту проблему с точки зрения рекурсии. Операция правого поворота `right_rotate(root)` принимает корневой узел поддерева, и в конце `return child` возвращает корневой узел поддерева после поворота. Связь между корневым узлом поддерева и его родительским узлом устанавливается после возврата из функции и не входит в область поддержки операции правого поворота.
**В**: В C++ функции разделены на `private` и `public`. Какие соображения здесь учитываются? Почему функции `height()` и `updateHeight()` размещены в `public` и `private` соответственно?
В основном это зависит от области использования метода. Если метод используется только внутри класса, то он проектируется как `private`. Например, отдельный вызов `updateHeight()` пользователем не имеет смысла, это всего лишь один из шагов в операциях вставки и удаления. А `height()` — это доступ к высоте узла, аналогично `vector.size()`, поэтому он установлен как `public` для удобства использования.
**В**: Как построить двоичное дерево поиска из набора входных данных? Важен ли выбор корневого узла?
Да, метод построения дерева уже представлен в методе `build_tree()` в коде двоичного дерева поиска. Что касается выбора корневого узла, мы обычно сортируем входные данные, затем выбираем средний элемент в качестве корневого узла и рекурсивно строим левое и правое поддеревья. Это позволяет максимально обеспечить сбалансированность дерева.
**В**: В Java обязательно ли использовать метод `equals()` для сравнения строк?
В Java для базовых типов данных `==` используется для сравнения значений двух переменных. Для ссылочных типов эти два символа работают по-разному.
- `==`: используется для сравнения того, указывают ли две переменные на один и тот же объект, т. е. одинаковы ли их позиции в памяти.
- `equals()`: используется для сравнения значений двух объектов.
Поэтому, если нужно сравнить значения, следует использовать `equals()`. Однако строки, инициализированные через `String a = "hi"; String b = "hi";`, хранятся в пуле строковых констант и указывают на один и тот же объект, поэтому также можно использовать `a == b` для сравнения содержимого двух строк.
**В**: До достижения самого нижнего уровня при обходе в ширину количество узлов в очереди равно $2^h$?
Да, например, для полного двоичного дерева высоты $h = 2$ общее количество узлов $n = 7$, тогда количество узлов на нижнем уровне $4 = 2^h = (n + 1) / 2$.