mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-18 01:36:07 +00:00
First version.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# Представление двоичного дерева с помощью массивом
|
||||
|
||||
При представлении в виде списка единицей хранения двоичного дерева является узел `TreeNode`, а узлы соединяются между собой указателями. В предыдущем разделе были рассмотрены основные операции с двоичным деревом, представленным в виде списка.
|
||||
|
||||
Можно ли представить двоичное дерево с помощью массива? Ответ положительный.
|
||||
|
||||
## Представление идеального двоичного дерева
|
||||
|
||||
Сначала рассмотрим простой пример. Если дано идеальное двоичное дерево и все его узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу соответствует уникальный индекс массива.
|
||||
|
||||
На основе свойств обхода по уровням можно вывести формулу отображения между индексом родительского узла и индексами дочерних узлов: **если индекс узла равен $i$, то индекс его левого дочернего узла равен $2i + 1$, а индекс правого дочернего узла равен $2i + 2$**. На рисунке ниже показаны отношения отображения между индексами различных узлов.
|
||||
|
||||
<!-- 🔴 俄文版缺失图片 -->
|
||||
<!-- 中文原文: -->
|
||||
|
||||
**Роль формулы отображения эквивалентна ссылкам на узлы (указателям) в списке**. Для любого узла в массиве мы можем получить доступ к его левому (правому) дочернему узлу с помощью формулы отображения.
|
||||
|
||||
## Представление произвольного двоичного дерева
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:完美二叉树是一个特例,在二叉树的中间层通常存在许多 `None` 。由于层序遍历序列并不包含这些 `None` ,因此我们无法仅凭该序列来推测 `None` 的数量和分布位置。**这意味着存在多种二叉树结构都符合该层序遍历序列**。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:如下图所示,给定一棵非完美二叉树,上述数组表示方法已经失效。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失图片 -->
|
||||
<!-- 中文原文: -->
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:为了解决此问题,**我们可以考虑在层序遍历序列中显式地写出所有 `None`** 。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。示例代码如下: -->
|
||||
|
||||
<!-- 🔴 俄文版缺失代码示例 -->
|
||||
<!-- 中文原文:=== "Python" ... [多语言代码示例] -->
|
||||
|
||||
<!-- 🔴 俄文版缺失图片 -->
|
||||
<!-- 中文原文: -->
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,`None` 只出现在最底层且靠右的位置,**因此所有 `None` 一定出现在层序遍历序列的末尾**。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:这意味着使用数组表示完全二叉树时,可以省略存储所有 `None` ,非常方便。下图给出了一个例子。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失图片 -->
|
||||
<!-- 中文原文: -->
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:以下代码实现了一棵基于数组表示的二叉树,包括以下几种操作。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失列表 -->
|
||||
<!-- 中文原文:- 给定某节点,获取它的值、左(右)子节点、父节点。- 获取前序遍历、中序遍历、后序遍历、层序遍历序列。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失代码引用 -->
|
||||
<!-- 中文原文:```src [file]{array_binary_tree}-[class]{array_binary_tree}-[func]{} ``` -->
|
||||
|
||||
## Преимущества и ограничения
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:二叉树的数组表示主要有以下优点。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失列表 -->
|
||||
<!-- 中文原文:- 数组存储在连续的内存空间中,对缓存友好,访问与遍历速度较快。- 不需要存储指针,比较节省空间。- 允许随机访问节点。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失此段落 -->
|
||||
<!-- 中文原文:然而,数组表示也存在一些局限性。 -->
|
||||
|
||||
<!-- 🔴 俄文版缺失列表 -->
|
||||
<!-- 中文原文:- 数组存储需要连续内存空间,因此不适合存储数据量过大的树。- 增删节点需要通过数组插入与删除操作实现,效率较低。- 当二叉树中存在大量 `None` 时,数组中包含的节点数据比重较低,空间利用率较低。 -->
|
||||
@@ -0,0 +1,358 @@
|
||||
# AVL-дерево *
|
||||
|
||||
В главе "Двоичное дерево поиска" мы упоминали, что после многократных операций вставки и удаления двоичное дерево поиска может выродиться в список. В этом случае временная сложность всех операций ухудшится с $O(\log n)$ до $O(n)$.
|
||||
|
||||
Как показано на рисунке ниже, после двух операций удаления узлов это двоичное дерево поиска выродится в список.
|
||||
|
||||

|
||||
|
||||
Например, в идеальном двоичном дереве, показанном на рисунке ниже, после вставки двух узлов дерево сильно наклонится влево, и временная сложность операции поиска также ухудшится.
|
||||
|
||||

|
||||
|
||||
В 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>"
|
||||

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

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

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

|
||||
|
||||
Как показано на рисунке ниже, когда узел `child` имеет правый дочерний узел (обозначим его как `grand_child`), необходимо добавить один шаг в правый поворот: сделать `grand_child` левым дочерним узлом `node`.
|
||||
|
||||

|
||||
|
||||
"Поворот вправо" — это образное выражение, на самом деле это реализуется путем изменения указателей узлов, код показан ниже:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{right_rotate}
|
||||
```
|
||||
|
||||
### Левый поворот
|
||||
|
||||
Соответственно, если рассмотреть "зеркальное отражение" вышеупомянутого несбалансированного двоичного дерева, необходимо выполнить "левый поворот", показанный на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
Аналогично, как показано на рисунке ниже, когда узел `child` имеет левый дочерний узел (обозначим его как `grand_child`), необходимо добавить один шаг в левый поворот: сделать `grand_child` правым дочерним узлом `node`.
|
||||
|
||||

|
||||
|
||||
Можно заметить, что **операции правого и левого поворотов логически зеркально симметричны, и две ситуации несбалансированности, которые они решают, также симметричны**. Основываясь на симметрии, нам нужно только заменить все `left` на `right` и все `right` на `left` в коде реализации правого поворота, чтобы получить код реализации левого поворота:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{left_rotate}
|
||||
```
|
||||
|
||||
### Сначала левый поворот, затем правый поворот
|
||||
|
||||
Для несбалансированного узла 3 на рисунке ниже использование только левого или правого поворота не может восстановить баланс поддерева. В этом случае необходимо сначала выполнить "левый поворот" для `child`, а затем "правый поворот" для `node`.
|
||||
|
||||

|
||||
|
||||
### Сначала правый поворот, затем левый поворот
|
||||
|
||||
Как показано на рисунке ниже, для зеркальной ситуации вышеупомянутого несбалансированного двоичного дерева необходимо сначала выполнить "правый поворот" для `child`, а затем "левый поворот" для `node`.
|
||||
|
||||

|
||||
|
||||
### Выбор поворота
|
||||
|
||||
На рисунке ниже показаны четыре ситуации несбалансированности, соответствующие вышеупомянутым случаям, для которых требуются операции правого поворота, сначала левого поворота затем правого поворота, сначала правого поворота затем левого поворота и левого поворота соответственно.
|
||||
|
||||

|
||||
|
||||
Как показано в таблице ниже, мы определяем, к какой ситуации на рисунке выше относится несбалансированный узел, путем оценки знака коэффициента балансировки несбалансированного узла и коэффициента балансировки дочернего узла с большей высотой.
|
||||
|
||||
<p align="center"> Таблица <id> Условия выбора четырех типов поворотов </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-деревом условия балансировки красно-черного дерева более мягкие, для операций вставки и удаления узлов требуется меньше операций поворота, средняя эффективность операций добавления и удаления узлов выше.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Двоичное дерево поиска
|
||||
|
||||
Как показано на рисунке ниже, <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)$ времени, без необходимости выполнения дополнительных операций сортировки, что очень эффективно.
|
||||
|
||||

|
||||
@@ -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 и все узлы ниже него».
|
||||
|
||||

|
||||
|
||||
## Основные понятия двоичного дерева
|
||||
|
||||
Основные понятия двоичного дерева показаны на рисунке ниже.
|
||||
|
||||
- <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>: количество ребер от самого удаленного листового узла до данного узла.
|
||||
|
||||

|
||||
|
||||
!!! 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
|
||||
|
||||
### Вставка и удаление узлов
|
||||
|
||||
Подобно спискам, в двоичном дереве вставку и удаление узлов можно выполнять путем изменения указателей. На рисунке ниже приведен пример.
|
||||
|
||||

|
||||
|
||||
=== "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.
|
||||
|
||||
Обход по уровням по своей сути является обходом в ширину, также называемым поиском в ширину, который характеризуется постепенно расширяющимся кольцом от центра к периферии.
|
||||
|
||||

|
||||
|
||||
### Код реализации
|
||||
|
||||
Обход в ширину обычно реализуется с использованием очереди. Очередь следует принципу «первый вошел -- первый вышел», а обход в ширину -- принципу «поэтапное продвижение», что делает их концептуально схожими. Ниже приведен код реализации.
|
||||
|
||||
```src
|
||||
[file]{binary_tree_bfs}-[class]{}-[func]{level_order}
|
||||
```
|
||||
|
||||
### Анализ сложности
|
||||
|
||||
**Временная сложность** *O*(*n*): каждый узел посещается один раз, что занимает *O*(*n*) времени выполнения, где *n* -- количество узлов.
|
||||
|
||||
**Пространственная сложность** *O*(*n*): в худшем случае, т. е. в полном двоичном дереве, до достижения самого нижнего уровня в очереди может находиться одновременно (*n* + 1)/2 узлов, что занимает *O*(*n*) пространства.
|
||||
|
||||
## Прямой, симметричный и обратный обходы
|
||||
|
||||
Прямой, симметричный и обратный обходы относятся к обходам в глубину, также называемым поиск в глубину, который характеризуется подходом «сначала до конца, затем возврат и продолжение».
|
||||
|
||||
На рис. 7.10 демонстрируется принцип работы обхода в глубину для двоичного дерева. **Обход в глубину можно представить как обход двоичного дерева по периметру**, при этом на каждом узле встречаются три позиции, соответствующие прямому, симметричному и обратному обходам.
|
||||
|
||||

|
||||
|
||||
### Код реализации
|
||||
|
||||
Поиск в глубину обычно реализуется на основе рекурсии.
|
||||
|
||||
```src
|
||||
[file]{binary_tree_dfs}-[class]{}-[func]{post_order}
|
||||
```
|
||||
|
||||
На рис. 7.11 демонстрируется рекурсивный процесс прямого обхода двоичного дерева, который можно разделить на два противоположных этапа: рекурсия и возврат.
|
||||
|
||||
1. Рекурсия означает начало нового метода, в процессе которого программа посещает следующий узел.
|
||||
|
||||
2. Возврат означает возвращение функции, что указывает на завершение посещения текущего узла.
|
||||
|
||||
=== "<1>"
|
||||

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

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

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

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
### Анализ сложности
|
||||
|
||||
**Временная сложность** *O*(*n*): все узлы посещаются один раз, что занимает *O*(*n*) времени.
|
||||
|
||||
**Пространственная сложность** *O*(*n*): в худшем случае, когда дерево вырождается в список, глубина рекурсии достигает *n*, система занимает *O*(*n*) пространства стека.
|
||||
@@ -0,0 +1,10 @@
|
||||
```markdown
|
||||
# Деревья
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Двоичное (бинарное) дерево -- это нелинейная структура данных, представляющая отношения между предками и потомками и отражающая логику «разделяй и властвуй».
|
||||
|
||||
```
|
||||
@@ -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$.
|
||||
Reference in New Issue
Block a user