Add ru version (#1865)
* Add Russian docs site baseline * Add Russian localized codebase * Polish Russian code wording * Update ru code translation. * Update code translation and chapter covers. * Fix pythontutor extraction. * Add README and landing page. * placeholder of profiles * Use figures of English version * Remove chapter paperbook
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,160 @@
|
||||
# Представление двоичного дерева массивом
|
||||
|
||||
В представлении через связную структуру единицей хранения двоичного дерева является узел `TreeNode` , а между узлами существуют связи через указатели. В предыдущем разделе были рассмотрены основные операции двоичного дерева в таком представлении.
|
||||
|
||||
Возникает вопрос: можно ли представить двоичное дерево с помощью массива? Ответ: да.
|
||||
|
||||
## Представление идеального двоичного дерева
|
||||
|
||||
Сначала разберем простой случай. Если дана идеальная двоичная структура и все ее узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу будет соответствовать единственный индекс массива.
|
||||
|
||||
Из свойств обхода по уровням можно вывести "формулу соответствия" между индексом родителя и индексами дочерних узлов: **если индекс некоторого узла равен $i$ , то индекс его левого дочернего узла равен $2i + 1$ , а правого - $2i + 2$** . На рисунке ниже показано соответствие между индексами разных узлов.
|
||||
|
||||

|
||||
|
||||
**Эта формула соответствия играет ту же роль, что и ссылки на узлы в связной структуре** . Имея любой узел в массиве, мы можем по формуле получить доступ к его левому и правому дочерним узлам.
|
||||
|
||||
## Представление произвольного двоичного дерева
|
||||
|
||||
Идеальное двоичное дерево - лишь частный случай; в обычной двоичной структуре на промежуточных уровнях часто существует множество `None` . Поскольку последовательность обхода по уровням не содержит этих `None` , мы не можем по одной лишь этой последовательности определить их количество и расположение. **Это означает, что одному и тому же обходу по уровням может соответствовать сразу несколько различных структур двоичного дерева**.
|
||||
|
||||
Как показано на рисунке ниже, для неполной двоичной структуры описанный выше способ представления массивом уже перестает работать.
|
||||
|
||||

|
||||
|
||||
Чтобы решить эту проблему, **мы можем явно записывать все `None` в последовательности обхода по уровням** . Как показано на рисунке ниже, после такой обработки последовательность обхода по уровням уже сможет однозначно задавать двоичное дерево. Пример кода приведен ниже:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
# Представление двоичного дерева массивом
|
||||
# Используем None для обозначения пустых позиций
|
||||
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используем максимальное значение int, INT_MAX, для обозначения пустых позиций
|
||||
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используя обертку Integer для int, можно применять null для обозначения пустых позиций
|
||||
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используя nullable-тип int? , можно применять null для обозначения пустых позиций
|
||||
int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используем срез типа any, чтобы можно было применять nil для обозначения пустых позиций
|
||||
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используя nullable-тип Int? , можно применять nil для обозначения пустых позиций
|
||||
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используем null для обозначения пустых позиций
|
||||
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используем null для обозначения пустых позиций
|
||||
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используя nullable-тип int? , можно применять null для обозначения пустых позиций
|
||||
List<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используем None для обозначения пустых позиций
|
||||
let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используем максимальное значение int для обозначения пустых позиций, поэтому узлы не должны принимать значение INT_MAX
|
||||
int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* Представление двоичного дерева массивом */
|
||||
// Используем null для обозначения пустых позиций
|
||||
val tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
### Представление двоичного дерева массивом ###
|
||||
# Используем nil для обозначения пустых позиций
|
||||
tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
|
||||
```
|
||||
|
||||

|
||||
|
||||
Стоит отметить, что **полное двоичное дерево очень удобно представлять массивом** . Если вспомнить определение полного двоичного дерева, то `None` появляются только на самом нижнем уровне и справа, **а значит, все `None` обязательно находятся в конце последовательности обхода по уровням**.
|
||||
|
||||
Это означает, что при представлении полного двоичного дерева массивом можно не хранить все `None` , что очень удобно. На рисунке ниже приведен пример.
|
||||
|
||||

|
||||
|
||||
Ниже приведен код реализации двоичного дерева, представленного массивом. Он включает следующие операции.
|
||||
|
||||
- Для заданного узла получить его значение, левого дочернего узла, правого дочернего узла и родительский узел.
|
||||
- Получить последовательности прямого, симметричного, обратного обходов и обхода по уровням.
|
||||
|
||||
```src
|
||||
[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{}
|
||||
```
|
||||
|
||||
## Преимущества и ограничения
|
||||
|
||||
Представление двоичного дерева массивом имеет в основном следующие преимущества.
|
||||
|
||||
- Массив хранится в непрерывной области памяти, хорошо работает с кешем и обеспечивает высокую скорость доступа и обхода.
|
||||
- Не нужно хранить указатели, поэтому память расходуется экономнее.
|
||||
- Разрешается произвольный доступ к узлам.
|
||||
|
||||
Однако у представления массивом есть и некоторые ограничения.
|
||||
|
||||
- Для хранения массива требуется непрерывная область памяти, поэтому такой способ не подходит для деревьев с очень большим объемом данных.
|
||||
- Добавление и удаление узлов приходится реализовывать через вставку и удаление элементов массива, а это не слишком эффективно.
|
||||
- Когда в двоичном дереве имеется большое число `None` , доля действительно полезных данных в массиве оказывается низкой, и эффективность использования пространства падает.
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,358 @@
|
||||
# AVL-дерево *
|
||||
|
||||
В разделе "Двоичное дерево поиска" мы упоминали, что после многократных операций вставки и удаления узлов двоичное дерево поиска может выродиться в связный список. В таком случае временная сложность всех операций ухудшается с $O(\log n)$ до $O(n)$ .
|
||||
|
||||
Как показано на рисунке ниже, после двух операций удаления узлов это двоичное дерево поиска вырождается в связный список.
|
||||
|
||||

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

|
||||
|
||||
В 1962 году Г. М. Adelson-Velsky и Е. М. Landis в статье "An algorithm for the organization of information" предложили <u>AVL-дерево</u>. В статье подробно описан набор операций, гарантирующий, что при непрерывном добавлении и удалении узлов AVL-дерево не вырождается, благодаря чему временная сложность различных операций сохраняется на уровне $O(\log n)$ . Иначе говоря, в сценариях, где часто выполняются вставка, удаление, поиск и изменение, AVL-дерево всегда поддерживает эффективную работу с данными и потому имеет высокую практическую ценность.
|
||||
|
||||
## Распространенные термины 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-деревом условия баланса у красно-черного дерева мягче, поэтому при вставке и удалении требуется меньше вращений, а средняя эффективность операций добавления и удаления выше.
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,129 @@
|
||||
# Двоичное дерево поиска
|
||||
|
||||
Как показано на рисунке ниже, <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)$ времени.
|
||||
|
||||
### Удаление узла
|
||||
|
||||
Сначала нужно найти в двоичном дереве целевой узел, а затем удалить его. Как и при вставке, после удаления необходимо сохранить свойство двоичного дерева поиска: "левое поддерево < корень < правое поддерево". Поэтому в зависимости от числа дочерних узлов у удаляемого узла, то есть для случаев со степенью 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)$ времени, без дополнительной сортировки, что очень эффективно.
|
||||
|
||||

|
||||
|
||||
## Эффективность двоичного дерева поиска
|
||||
|
||||
Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Из таблицы ниже видно, что временная сложность операций двоичного дерева поиска имеет логарифмический порядок, поэтому его производительность стабильна и высока. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.
|
||||
|
||||
<p align="center"> Таблица <id> Сравнение эффективности массива и дерева поиска </p>
|
||||
|
||||
| | Неупорядоченный массив | Двоичное дерево поиска |
|
||||
| -------- | ---------------------- | ---------------------- |
|
||||
| Поиск элемента | $O(n)$ | $O(\log n)$ |
|
||||
| Вставка элемента | $O(1)$ | $O(\log n)$ |
|
||||
| Удаление элемента | $O(n)$ | $O(\log n)$ |
|
||||
|
||||
В идеальном случае двоичное дерево поиска является "сбалансированным", и тогда любой узел можно найти за $\log n$ итераций.
|
||||
|
||||
Однако если в двоичное дерево поиска непрерывно вставлять и удалять узлы, оно может выродиться в связный список, как показано на рисунке ниже. Тогда временная сложность различных операций тоже вырождается до $O(n)$ .
|
||||
|
||||

|
||||
|
||||
## Типичные применения двоичного дерева поиска
|
||||
|
||||
- Используется как многоуровневый индекс в системах, обеспечивая эффективный поиск, вставку и удаление.
|
||||
- Служит базовой структурой данных для некоторых поисковых алгоритмов.
|
||||
- Применяется для хранения потока данных в отсортированном состоянии.
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,674 @@
|
||||
# Двоичное дерево
|
||||
|
||||
<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%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20%D1%83%D0%B7%D0%BB%D0%B0%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%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%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BB%D0%B5%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%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%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%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%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%20%28%D1%83%D0%BA%D0%B0%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D0%B8%29%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%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20%D1%83%D0%B7%D0%BB%D0%B0%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%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%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BB%D0%B5%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%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%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%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%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%20%28%D1%83%D0%BA%D0%B0%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D0%B8%29%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%0A%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0%20%D0%B8%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%BE%D0%B2%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%20P%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20n1%20-%3E%20n2%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%83%D0%B7%D0%B5%D0%BB%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
!!! tip
|
||||
|
||||
Обрати внимание: вставка узла может изменить исходную логическую структуру двоичного дерева, а удаление узла обычно означает удаление этого узла вместе со всеми его поддеревьями. Поэтому в двоичном дереве операции вставки и удаления обычно являются частью более крупного набора операций, который и реализует осмысленное действие.
|
||||
|
||||
## Распространенные типы двоичных деревьев
|
||||
|
||||
### Идеальное двоичное дерево
|
||||
|
||||
Как показано на рисунке ниже, <u>идеальное двоичное дерево (perfect binary tree)</u> полностью заполнено на всех уровнях. В идеальном двоичном дереве степень листовых узлов равна $0$ , а у всех остальных узлов степень равна $2$ ; если высота дерева равна $h$ , то общее число узлов равно $2^{h+1} - 1$ , что образует стандартную экспоненциальную зависимость и отражает часто встречающееся в природе явление клеточного деления.
|
||||
|
||||
!!! tip
|
||||
|
||||
Обрати внимание: в китайскоязычном сообществе идеальное двоичное дерево часто называют <u>полностью заполненным двоичным деревом</u>.
|
||||
|
||||

|
||||
|
||||
### Полное двоичное дерево
|
||||
|
||||
Как показано на рисунке ниже, <u>полное двоичное дерево (complete binary tree)</u> допускает неполное заполнение только на самом нижнем уровне, причем узлы этого уровня должны непрерывно заполняться слева направо. Обрати внимание: идеальное двоичное дерево тоже является полным двоичным деревом.
|
||||
|
||||

|
||||
|
||||
### Строгое двоичное дерево
|
||||
|
||||
Как показано на рисунке ниже, <u>строгое двоичное дерево (full binary tree)</u> имеет у всех нелистовых узлов ровно двух дочерних узлов.
|
||||
|
||||

|
||||
|
||||
### Сбалансированное двоичное дерево
|
||||
|
||||
Как показано на рисунке ниже, в <u>сбалансированном двоичном дереве (balanced binary tree)</u> для любого узла абсолютное значение разности высот левого и правого поддеревьев не превышает 1 .
|
||||
|
||||

|
||||
|
||||
## Вырождение двоичного дерева
|
||||
|
||||
На рисунке ниже показаны идеальная структура двоичного дерева и вырожденная структура. Когда каждый уровень двоичного дерева полностью заполнен узлами, мы получаем "идеальное двоичное дерево"; когда же все узлы смещаются к одной стороне, двоичное дерево вырождается в "связный список".
|
||||
|
||||
- Идеальное двоичное дерево соответствует лучшему случаю и позволяет полностью раскрыть преимущества двоичного дерева с точки зрения "разделяй и властвуй".
|
||||
- Связный список представляет противоположную крайность: все операции становятся линейными, а временная сложность деградирует до $O(n)$ .
|
||||
|
||||

|
||||
|
||||
Как показано в таблице ниже, в лучшем и худшем случаях число листовых узлов, общее число узлов, высота и другие характеристики двоичного дерева достигают максимума или минимума.
|
||||
|
||||
<p align="center"> Таблица <id> Лучший и худший случаи структуры двоичного дерева </p>
|
||||
|
||||
| | Идеальное двоичное дерево | Связный список |
|
||||
| --------------------------- | ------------------------- | -------------- |
|
||||
| Число узлов на уровне $i$ | $2^{i-1}$ | $1$ |
|
||||
| Число листьев у дерева высоты $h$ | $2^h$ | $1$ |
|
||||
| Общее число узлов у дерева высоты $h$ | $2^{h+1} - 1$ | $h + 1$ |
|
||||
| Высота дерева с $n$ узлами | $\log_2 (n+1) - 1$ | $n - 1$ |
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,89 @@
|
||||
# Обход двоичного дерева
|
||||
|
||||
С точки зрения физической структуры дерево представляет собой разновидность структуры данных на основе связей, поэтому его обход выполняется через последовательный доступ к узлам по указателям. Однако дерево является нелинейной структурой данных, а значит, его обход сложнее, чем обход связного списка, и для него требуется использовать поисковые алгоритмы.
|
||||
|
||||
К распространенным способам обхода двоичного дерева относятся обход по уровням, прямой обход, симметричный обход и обратный обход.
|
||||
|
||||
## Обход по уровням
|
||||
|
||||
Как показано на рисунке ниже, <u>обход по уровням (level-order traversal)</u> проходит двоичное дерево сверху вниз по уровням и на каждом уровне посещает узлы слева направо.
|
||||
|
||||
По своей сути обход по уровням относится к <u>обходу в ширину (breadth-first traversal)</u>, также называемому <u>поиском в ширину (breadth-first search, BFS)</u>; он отражает идею "расширяться слой за слоем наружу".
|
||||
|
||||

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

|
||||
|
||||
### Код реализации
|
||||
|
||||
Поиск в глубину обычно реализуется через рекурсию:
|
||||
|
||||
```src
|
||||
[file]{binary_tree_dfs}-[class]{}-[func]{post_order}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
Поиск в глубину можно реализовать и итеративно; заинтересованные читатели могут изучить это самостоятельно.
|
||||
|
||||
На рисунках ниже показан рекурсивный процесс прямого обхода двоичного дерева. Его можно разделить на две противоположные части: "вход в рекурсию" и "возврат".
|
||||
|
||||
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,9 @@
|
||||
# Деревья
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Высокое дерево полно жизни: мощные корни, густая крона и раскидистые ветви.
|
||||
|
||||
Оно наглядно показывает нам форму данных, построенную на принципе "разделяй и властвуй".
|
||||
@@ -0,0 +1,54 @@
|
||||
# Краткие итоги
|
||||
|
||||
### Основные моменты
|
||||
|
||||
- Двоичное дерево - это нелинейная структура данных, отражающая логику "разделения надвое". Каждый узел двоичного дерева содержит значение и два указателя, которые соответственно ведут к левому и правому дочерним узлам.
|
||||
- Для любого узла двоичного дерева дерево, образованное его левым (правым) дочерним узлом и всеми нижележащими узлами, называется левым (правым) поддеревом этого узла.
|
||||
- К связанным с двоичным деревом терминам относятся корневой узел, листовой узел, уровень, степень, ребро, высота, глубина и так далее.
|
||||
- Инициализация двоичного дерева, вставка узлов и удаление узлов похожи по способу реализации на операции со связным списком.
|
||||
- К распространенным видам двоичного дерева относятся идеальное двоичное дерево, полное двоичное дерево, строгое двоичное дерево и сбалансированное двоичное дерево. Идеальное двоичное дерево - наиболее желательное состояние, а связный список - худший случай после вырождения.
|
||||
- Двоичное дерево можно представить массивом: значения узлов и пустые позиции располагаются в порядке обхода по уровням, а связи между родителем и детьми реализуются через отображение индексов.
|
||||
- Обход двоичного дерева по уровням является методом поиска в ширину; он отражает идею "расширяться слой за слоем наружу" и обычно реализуется через очередь.
|
||||
- Прямой, симметричный и обратный обходы относятся к поиску в глубину; они отражают идею "сначала дойти до конца, затем откатиться и продолжить" и обычно реализуются рекурсивно.
|
||||
- Двоичное дерево поиска - это эффективная структура данных для поиска элементов; его поиск, вставка и удаление имеют временную сложность $O(\log n)$ . Когда двоичное дерево поиска вырождается в связный список, все эти сложности деградируют до $O(n)$ .
|
||||
- AVL-дерево, также называемое сбалансированным двоичным деревом поиска, с помощью вращений гарантирует, что после постоянных вставок и удалений узлов дерево остается сбалансированным.
|
||||
- Вращения AVL-дерева включают правое вращение, левое вращение, сначала правое затем левое и сначала левое затем правое. После вставки или удаления узла AVL-дерево выполняет вращения снизу вверх, чтобы снова восстановить баланс.
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: Для двоичного дерева, состоящего из одного узла, высота дерева и глубина корня обе равны $0$ ?
|
||||
|
||||
Да, потому что высота и глубина обычно определяются как "число пройденных ребер".
|
||||
|
||||
**Q**: Вставка и удаление в двоичном дереве обычно выполняются в составе набора операций. Что именно означает этот "набор операций"? Можно ли понимать это как освобождение ресурсов у дочерних узлов ресурса?
|
||||
|
||||
Возьмем в качестве примера двоичное дерево поиска: операция удаления узла делится на три случая, и каждый из этих случаев требует нескольких последовательных шагов работы с узлами.
|
||||
|
||||
**Q**: Почему у DFS для двоичного дерева есть три порядка: прямой, симметричный и обратный? Для чего они нужны?
|
||||
|
||||
Подобно прямому и обратному обходу массива, прямой, симметричный и обратный обходы - это три способа обхода двоичного дерева, с помощью которых можно получить результаты в определенном порядке. Например, в двоичном дереве поиска, где соблюдается отношение `значение левого дочернего узла < значение корня < значение правого дочернего узла` , если обходить дерево с приоритетом "лево $\rightarrow$ корень $\rightarrow$ право", то получится упорядоченная последовательность узлов.
|
||||
|
||||
**Q**: Правое вращение работает с отношениями между `node` , `child` и `grand_child` . А связь между `node` и его исходным родителем разве не нужно поддерживать? После правого вращения она ведь не оборвется?
|
||||
|
||||
На это нужно смотреть с точки зрения рекурсии. В правое вращение `right_rotate(root)` передается корень поддерева, а затем через `return child` возвращается корень этого поддерева уже после вращения. Соединение между новым корнем поддерева и его родителем восстанавливается после возврата функции и не входит в обязанности самой операции правого вращения.
|
||||
|
||||
**Q**: В C++ функции делятся на `private` и `public` . Какая логика стоит за этим? Почему `height()` и `updateHeight()` помещают в разные области видимости?
|
||||
|
||||
Главный критерий - область использования метода. Если метод нужен только внутри класса, его следует проектировать как `private` . Например, самостоятельный вызов `updateHeight()` пользователем не имеет смысла: это лишь один из шагов внутри вставки или удаления. А `height()` используется для чтения высоты узла, подобно `vector.size()` , поэтому его разумно делать `public` .
|
||||
|
||||
**Q**: Как построить двоичное дерево поиска из набора входных данных? Важен ли выбор корневого узла?
|
||||
|
||||
Да, важен. Способ построения дерева уже показан в методе `build_tree()` в коде двоичного дерева поиска. Что касается выбора корня, обычно входные данные сортируют, берут средний элемент как корень, а затем рекурсивно строят левое и правое поддеревья. Это позволяет в наибольшей степени сохранить баланс дерева.
|
||||
|
||||
**Q**: Нужно ли в Java всегда использовать `equals()` для сравнения строк?
|
||||
|
||||
В Java для базовых типов `==` используется, чтобы сравнивать, равны ли значения двух переменных. Для ссылочных типов логика у этих двух способов уже разная.
|
||||
|
||||
- `==` : сравнивает, ссылаются ли две переменные на один и тот же объект, то есть совпадает ли их адрес в памяти.
|
||||
- `equals()`: сравнивает, равны ли значения двух объектов.
|
||||
|
||||
Поэтому если нужно сравнить значения, то следует использовать `equals()` . Но строки, инициализированные как `String a = "hi"; String b = "hi";` , хранятся в строковом пуле констант и указывают на один и тот же объект, поэтому в таком случае `a == b` тоже может дать истинный результат при сравнении содержимого.
|
||||
|
||||
**Q**: До достижения самого нижнего уровня при обходе в ширину число узлов в очереди равно $2^h$ ?
|
||||
|
||||
Да. Например, для полного двоичного дерева высоты $h = 2$ общее число узлов равно $n = 7$ , а число узлов на нижнем уровне равно $4 = 2^h = (n + 1) / 2$ .
|
||||