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
This commit is contained in:
Yudong Jin
2026-03-28 04:24:07 +08:00
committed by GitHub
parent 2ca570cc33
commit 772183705e
1958 changed files with 108186 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,160 @@
# Представление двоичного дерева массивом
В представлении через связную структуру единицей хранения двоичного дерева является узел `TreeNode` , а между узлами существуют связи через указатели. В предыдущем разделе были рассмотрены основные операции двоичного дерева в таком представлении.
Возникает вопрос: можно ли представить двоичное дерево с помощью массива? Ответ: да.
## Представление идеального двоичного дерева
Сначала разберем простой случай. Если дана идеальная двоичная структура и все ее узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу будет соответствовать единственный индекс массива.
Из свойств обхода по уровням можно вывести "формулу соответствия" между индексом родителя и индексами дочерних узлов: **если индекс некоторого узла равен $i$ , то индекс его левого дочернего узла равен $2i + 1$ , а правого - $2i + 2$** . На рисунке ниже показано соответствие между индексами разных узлов.
![Представление идеального двоичного дерева массивом](array_representation_of_tree.assets/array_representation_binary_tree.png)
**Эта формула соответствия играет ту же роль, что и ссылки на узлы в связной структуре** . Имея любой узел в массиве, мы можем по формуле получить доступ к его левому и правому дочерним узлам.
## Представление произвольного двоичного дерева
Идеальное двоичное дерево - лишь частный случай; в обычной двоичной структуре на промежуточных уровнях часто существует множество `None` . Поскольку последовательность обхода по уровням не содержит этих `None` , мы не можем по одной лишь этой последовательности определить их количество и расположение. **Это означает, что одному и тому же обходу по уровням может соответствовать сразу несколько различных структур двоичного дерева**.
Как показано на рисунке ниже, для неполной двоичной структуры описанный выше способ представления массивом уже перестает работать.
![Одной последовательности обхода по уровням соответствуют разные двоичные структуры](array_representation_of_tree.assets/array_representation_without_empty.png)
Чтобы решить эту проблему, **мы можем явно записывать все `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]
```
![Представление произвольного двоичного дерева массивом](array_representation_of_tree.assets/array_representation_with_empty.png)
Стоит отметить, что **полное двоичное дерево очень удобно представлять массивом** . Если вспомнить определение полного двоичного дерева, то `None` появляются только на самом нижнем уровне и справа, **а значит, все `None` обязательно находятся в конце последовательности обхода по уровням**.
Это означает, что при представлении полного двоичного дерева массивом можно не хранить все `None` , что очень удобно. На рисунке ниже приведен пример.
![Представление полного двоичного дерева массивом](array_representation_of_tree.assets/array_representation_complete_binary_tree.png)
Ниже приведен код реализации двоичного дерева, представленного массивом. Он включает следующие операции.
- Для заданного узла получить его значение, левого дочернего узла, правого дочернего узла и родительский узел.
- Получить последовательности прямого, симметричного, обратного обходов и обхода по уровням.
```src
[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{}
```
## Преимущества и ограничения
Представление двоичного дерева массивом имеет в основном следующие преимущества.
- Массив хранится в непрерывной области памяти, хорошо работает с кешем и обеспечивает высокую скорость доступа и обхода.
- Не нужно хранить указатели, поэтому память расходуется экономнее.
- Разрешается произвольный доступ к узлам.
Однако у представления массивом есть и некоторые ограничения.
- Для хранения массива требуется непрерывная область памяти, поэтому такой способ не подходит для деревьев с очень большим объемом данных.
- Добавление и удаление узлов приходится реализовывать через вставку и удаление элементов массива, а это не слишком эффективно.
- Когда в двоичном дереве имеется большое число `None` , доля действительно полезных данных в массиве оказывается низкой, и эффективность использования пространства падает.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+358
View File
@@ -0,0 +1,358 @@
# AVL-дерево *
В разделе "Двоичное дерево поиска" мы упоминали, что после многократных операций вставки и удаления узлов двоичное дерево поиска может выродиться в связный список. В таком случае временная сложность всех операций ухудшается с $O(\log n)$ до $O(n)$ .
Как показано на рисунке ниже, после двух операций удаления узлов это двоичное дерево поиска вырождается в связный список.
![Деградация AVL-дерева после удаления узлов](avl_tree.assets/avltree_degradation_from_removing_node.png)
Другой пример: если в идеальное двоичное дерево, показанное на рисунке ниже, вставить два узла, то дерево сильно наклонится влево, а временная сложность поиска тоже ухудшится.
![Деградация AVL-дерева после вставки узлов](avl_tree.assets/avltree_degradation_from_inserting_node.png)
В 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>"
![Шаги правого вращения](avl_tree.assets/avltree_right_rotate_step1.png)
=== "<2>"
![avltree_right_rotate_step2](avl_tree.assets/avltree_right_rotate_step2.png)
=== "<3>"
![avltree_right_rotate_step3](avl_tree.assets/avltree_right_rotate_step3.png)
=== "<4>"
![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png)
Как показано на рисунке ниже, когда у узла `child` есть правый дочерний узел, который мы обозначим как `grand_child` , в правое вращение нужно добавить еще один шаг: сделать `grand_child` левым дочерним узлом `node` .
![Правое вращение при наличии grand_child](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
"Поворот вправо" - это лишь образное описание; в реальности он реализуется через изменение указателей узлов. Код приведен ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{right_rotate}
```
### Левое вращение
Соответственно, если рассмотреть "зеркальную" версию приведенного выше разбалансированного двоичного дерева, то понадобится выполнить "левое вращение", показанное на рисунке ниже.
![Левое вращение](avl_tree.assets/avltree_left_rotate.png)
По той же причине, когда у узла `child` есть левый дочерний узел, который обозначим как `grand_child` , в левое вращение также требуется добавить шаг: сделать `grand_child` правым дочерним узлом `node` .
![Левое вращение при наличии grand_child](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
Можно заметить, что **операции правого и левого вращения логически зеркально симметричны, и два вида разбаланса, которые они исправляют, тоже симметричны**. Поэтому, опираясь на эту симметрию, достаточно заменить в коде правого вращения все `left` на `right` , а все `right` на `left` , чтобы получить реализацию левого вращения:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{left_rotate}
```
### Сначала левое, затем правое вращение
Для разбалансированного узла 3 на рисунке ниже ни одно лишь левое вращение, ни одно лишь правое вращение не способны вернуть поддерево в баланс. В этом случае нужно сначала выполнить "левое вращение" для `child` , а затем выполнить "правое вращение" для `node` .
![Сначала левое, затем правое вращение](avl_tree.assets/avltree_left_right_rotate.png)
### Сначала правое, затем левое вращение
Как показано на рисунке ниже, для зеркальной ситуации предыдущего разбалансированного двоичного дерева нужно сначала выполнить "правое вращение" для `child` , а затем "левое вращение" для `node` .
![Сначала правое, затем левое вращение](avl_tree.assets/avltree_right_left_rotate.png)
### Выбор вращения
Четыре вида разбаланса, показанные на рисунке ниже, по одному соответствуют рассмотренным выше случаям; для них соответственно требуются правое вращение, сначала левое затем правое, сначала правое затем левое и левое вращение.
![Четыре случая вращений AVL-дерева](avl_tree.assets/avltree_rotation_cases.png)
Как показано в таблице ниже, мы определяем, какому из этих четырех случаев соответствует разбалансированный узел, по знаку баланс-фактора самого разбалансированного узла и по знаку баланс-фактора дочернего узла на более высокой стороне.
<p align="center"> Таблица <id> &nbsp; Условия выбора для четырех случаев вращений </p>
| Баланс-фактор разбалансированного узла | Баланс-фактор дочернего узла | Какое вращение использовать |
| -------------------------------------- | ---------------------------- | --------------------------- |
| $> 1$ (левостороннее дерево) | $\geq 0$ | Правое вращение |
| $> 1$ (левостороннее дерево) | $<0$ | Сначала левое, затем правое |
| $< -1$ (правостороннее дерево) | $\leq 0$ | Левое вращение |
| $< -1$ (правостороннее дерево) | $>0$ | Сначала правое, затем левое |
Для удобства мы инкапсулируем операцию вращения в отдельную функцию. **С помощью этой функции можно выполнить корректное вращение для любой ситуации разбаланса и снова привести узел в сбалансированное состояние**. Код приведен ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{rotate}
```
## Распространенные операции AVL-дерева
### Вставка узла
Операция вставки узла в AVL-дерево по основному процессу похожа на вставку в двоичное дерево поиска. Единственная разница состоит в том, что после вставки в AVL-дерево на пути от вставленного узла к корню может появиться цепочка разбалансированных узлов. Поэтому **начиная от этого узла, мы должны выполнять вращения снизу вверх, чтобы вернуть в баланс все разбалансированные узлы**. Код приведен ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{insert_helper}
```
### Удаление узла
Аналогично, на основе метода удаления узла из двоичного дерева поиска нужно добавить вращения снизу вверх, чтобы восстановить баланс всех разбалансированных узлов. Код приведен ниже:
```src
[file]{avl_tree}-[class]{avl_tree}-[func]{remove_helper}
```
### Поиск узла
Операция поиска узла в AVL-дереве совпадает с поиском в двоичном дереве поиска, поэтому здесь она повторно не рассматривается.
## Типичные применения AVL-дерева
- Организация и хранение больших массивов данных, особенно в сценариях с частым поиском и относительно редкими вставками и удалениями.
- Использование при построении индексных систем в базах данных.
- Красно-черное дерево тоже является распространенным видом сбалансированного двоичного дерева поиска. По сравнению с AVL-деревом условия баланса у красно-черного дерева мягче, поэтому при вставке и удалении требуется меньше вращений, а средняя эффективность операций добавления и удаления выше.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+129
View File
@@ -0,0 +1,129 @@
# Двоичное дерево поиска
Как показано на рисунке ниже, <u>двоичное дерево поиска (binary search tree)</u> удовлетворяет следующим условиям.
1. Для корневого узла все значения в левом поддереве меньше значения корневого узла, а все значения в правом поддереве больше значения корневого узла.
2. Левое и правое поддеревья любого узла также являются двоичными деревьями поиска, то есть тоже удовлетворяют условию `1.` .
![Двоичное дерево поиска](binary_search_tree.assets/binary_search_tree.png)
## Операции с двоичным деревом поиска
Мы инкапсулируем двоичное дерево поиска в класс `BinarySearchTree` и объявляем переменную-член `root` , которая указывает на корневой узел дерева.
### Поиск узла
Для заданного целевого значения узла `num` можно выполнить поиск, опираясь на свойства двоичного дерева поиска. Как показано на рисунках ниже, мы объявляем узел `cur` , стартуем от корня дерева `root` и циклически сравниваем значения `cur.val` и `num` .
- Если `cur.val < num` , это означает, что целевой узел находится в правом поддереве `cur` , поэтому выполняем `cur = cur.right` .
- Если `cur.val > num` , это означает, что целевой узел находится в левом поддереве `cur` , поэтому выполняем `cur = cur.left` .
- Если `cur.val = num` , это означает, что целевой узел найден, и мы выходим из цикла, возвращая этот узел.
=== "<1>"
![Пример поиска узла в двоичном дереве поиска](binary_search_tree.assets/bst_search_step1.png)
=== "<2>"
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png)
=== "<3>"
![bst_search_step3](binary_search_tree.assets/bst_search_step3.png)
=== "<4>"
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png)
Операция поиска в двоичном дереве поиска работает по тому же принципу, что и бинарный поиск: на каждом шаге она отбрасывает половину вариантов. Число итераций не превосходит высоты двоичного дерева, а когда дерево сбалансировано, требуется $O(\log n)$ времени. Пример кода приведен ниже:
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
```
### Вставка узла
Пусть дан элемент `num` , который нужно вставить. Чтобы сохранить свойство двоичного дерева поиска "левое поддерево < корень < правое поддерево", процесс вставки выглядит следующим образом.
1. **Найти позицию для вставки**: как и в операции поиска, начиная от корня, мы циклически спускаемся вниз в зависимости от соотношения между текущим значением узла и `num` , пока не выйдем за листовой узел (то есть не дойдем до `None` ).
2. **Вставить узел в найденную позицию**: инициализировать узел `num` и поставить его на место этого `None` .
![Вставка узла в двоичное дерево поиска](binary_search_tree.assets/bst_insert.png)
В реализации кода нужно обратить внимание на следующие два момента.
- Двоичное дерево поиска не допускает дублирующихся узлов, иначе его определение будет нарушено. Поэтому если вставляемый узел уже существует в дереве, вставка не выполняется и функция сразу возвращается.
- Чтобы реализовать вставку, нам нужно использовать узел `pre` для сохранения узла предыдущей итерации цикла. Тогда, когда обход дойдет до `None` , мы сможем получить его родителя и завершить вставку.
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
```
Как и поиск узла, вставка узла требует $O(\log n)$ времени.
### Удаление узла
Сначала нужно найти в двоичном дереве целевой узел, а затем удалить его. Как и при вставке, после удаления необходимо сохранить свойство двоичного дерева поиска: "левое поддерево < корень < правое поддерево". Поэтому в зависимости от числа дочерних узлов у удаляемого узла, то есть для случаев со степенью 0, 1 и 2, выполняются разные операции удаления.
Как показано на рисунке ниже, когда степень удаляемого узла равна $0$ , это значит, что узел является листом и может быть удален напрямую.
![Удаление узла в двоичном дереве поиска (степень 0)](binary_search_tree.assets/bst_remove_case1.png)
Как показано на рисунке ниже, когда степень удаляемого узла равна $1$ , достаточно заменить удаляемый узел его дочерним узлом.
![Удаление узла в двоичном дереве поиска (степень 1)](binary_search_tree.assets/bst_remove_case2.png)
Когда степень удаляемого узла равна $2$ , мы уже не можем удалить его напрямую и должны использовать для замены другой узел. Чтобы сохранить свойство двоичного дерева поиска "левое поддерево $<$ корень $<$ правое поддерево", **этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева**.
Предположим, мы выбираем минимальный узел правого поддерева, то есть следующий узел в симметричном обходе. Тогда процесс удаления выглядит так.
1. Найти следующий узел в "последовательности симметричного обхода" для удаляемого узла и обозначить его как `tmp` .
2. Значением `tmp` перезаписать значение удаляемого узла, а затем рекурсивно удалить узел `tmp` из дерева.
=== "<1>"
![Удаление узла в двоичном дереве поиска (степень 2)](binary_search_tree.assets/bst_remove_case3_step1.png)
=== "<2>"
![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png)
=== "<3>"
![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png)
=== "<4>"
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png)
Операция удаления узла также требует $O(\log n)$ времени, где поиск удаляемого узла стоит $O(\log n)$ , а получение следующего узла симметричного обхода также требует $O(\log n)$ . Пример кода приведен ниже:
```src
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
```
### Упорядоченность симметричного обхода
Как показано на рисунке ниже, симметричный обход двоичного дерева следует порядку "лево $\rightarrow$ корень $\rightarrow$ право", а двоичное дерево поиска удовлетворяет соотношению "левый дочерний узел $<$ корень $<$ правый дочерний узел".
Это означает, что при симметричном обходе двоичного дерева поиска мы всегда сначала будем посещать следующий минимальный узел, и отсюда получается важное свойство: **последовательность симметричного обхода двоичного дерева поиска является возрастающей**.
Используя это свойство возрастающей последовательности симметричного обхода, мы можем получить отсортированные данные из двоичного дерева поиска всего за $O(n)$ времени, без дополнительной сортировки, что очень эффективно.
![Последовательность симметричного обхода двоичного дерева поиска](binary_search_tree.assets/bst_inorder_traversal.png)
## Эффективность двоичного дерева поиска
Для заданного набора данных можно рассмотреть хранение либо в массиве, либо в двоичном дереве поиска. Из таблицы ниже видно, что временная сложность операций двоичного дерева поиска имеет логарифмический порядок, поэтому его производительность стабильна и высока. Только в сценариях с очень частыми вставками и редкими поисками и удалениями массив может быть эффективнее, чем двоичное дерево поиска.
<p align="center"> Таблица <id> &nbsp; Сравнение эффективности массива и дерева поиска </p>
| | Неупорядоченный массив | Двоичное дерево поиска |
| -------- | ---------------------- | ---------------------- |
| Поиск элемента | $O(n)$ | $O(\log n)$ |
| Вставка элемента | $O(1)$ | $O(\log n)$ |
| Удаление элемента | $O(n)$ | $O(\log n)$ |
В идеальном случае двоичное дерево поиска является "сбалансированным", и тогда любой узел можно найти за $\log n$ итераций.
Однако если в двоичное дерево поиска непрерывно вставлять и удалять узлы, оно может выродиться в связный список, как показано на рисунке ниже. Тогда временная сложность различных операций тоже вырождается до $O(n)$ .
![Деградация двоичного дерева поиска](binary_search_tree.assets/bst_degradation.png)
## Типичные применения двоичного дерева поиска
- Используется как многоуровневый индекс в системах, обеспечивая эффективный поиск, вставку и удаление.
- Служит базовой структурой данных для некоторых поисковых алгоритмов.
- Применяется для хранения потока данных в отсортированном состоянии.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+674
View File
@@ -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 и дерево ниже него".
![Родительский узел, дочерние узлы и поддеревья](binary_tree.assets/binary_tree_definition.png)
## Распространенные термины двоичного дерева
Распространенные термины двоичного дерева показаны на рисунке ниже.
- <u>Корневой узел (root node)</u>: узел, расположенный на верхнем уровне двоичного дерева и не имеющий родительского узла.
- <u>Листовой узел (leaf node)</u>: узел без дочерних узлов; оба его указателя направлены на `None` .
- <u>Ребро (edge)</u>: отрезок, соединяющий два узла, то есть ссылка (указатель) между узлами.
- <u>Уровень (level)</u> узла: увеличивается сверху вниз; уровень корневого узла равен 1 .
- <u>Степень (degree)</u> узла: число дочерних узлов данного узла. В двоичном дереве возможны степени 0, 1, 2 .
- <u>Высота (height)</u> двоичного дерева: число ребер от корневого узла до самого удаленного листового узла.
- <u>Глубина (depth)</u> узла: число ребер от корневого узла до данного узла.
- <u>Высота (height)</u> узла: число ребер от самого удаленного листового узла до данного узла.
![Распространенные термины двоичного дерева](binary_tree.assets/binary_tree_terminology.png)
!!! tip
Обрати внимание: обычно под "высотой" и "глубиной" понимают "число пройденных ребер", но в некоторых задачах или учебниках их могут определять как "число пройденных узлов". В таком случае и высоту, и глубину нужно увеличить на 1 .
## Базовые операции двоичного дерева
### Инициализация двоичного дерева
Как и в связном списке, сначала инициализируются узлы, а затем между ними строятся ссылки (указатели).
=== "Python"
```python title="binary_tree.py"
# Инициализация двоичного дерева
# Инициализация узлов
n1 = TreeNode(val=1)
n2 = TreeNode(val=2)
n3 = TreeNode(val=3)
n4 = TreeNode(val=4)
n5 = TreeNode(val=5)
# Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "C++"
```cpp title="binary_tree.cpp"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode* n1 = new TreeNode(1);
TreeNode* n2 = new TreeNode(2);
TreeNode* n3 = new TreeNode(3);
TreeNode* n4 = new TreeNode(4);
TreeNode* n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
```
=== "Java"
```java title="binary_tree.java"
// Инициализация узлов
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "C#"
```csharp title="binary_tree.cs"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode n1 = new(1);
TreeNode n2 = new(2);
TreeNode n3 = new(3);
TreeNode n4 = new(4);
TreeNode n5 = new(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Go"
```go title="binary_tree.go"
/* Инициализация двоичного дерева */
// Инициализация узлов
n1 := NewTreeNode(1)
n2 := NewTreeNode(2)
n3 := NewTreeNode(3)
n4 := NewTreeNode(4)
n5 := NewTreeNode(5)
// Построение ссылок (указателей) между узлами
n1.Left = n2
n1.Right = n3
n2.Left = n4
n2.Right = n5
```
=== "Swift"
```swift title="binary_tree.swift"
// Инициализация узлов
let n1 = TreeNode(x: 1)
let n2 = TreeNode(x: 2)
let n3 = TreeNode(x: 3)
let n4 = TreeNode(x: 4)
let n5 = TreeNode(x: 5)
// Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "JS"
```javascript title="binary_tree.js"
/* Инициализация двоичного дерева */
// Инициализация узлов
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "TS"
```typescript title="binary_tree.ts"
/* Инициализация двоичного дерева */
// Инициализация узлов
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Dart"
```dart title="binary_tree.dart"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// Построение ссылок (указателей) между узлами
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Rust"
```rust title="binary_tree.rs"
// Инициализация узлов
let n1 = TreeNode::new(1);
let n2 = TreeNode::new(2);
let n3 = TreeNode::new(3);
let n4 = TreeNode::new(4);
let n5 = TreeNode::new(5);
// Построение ссылок (указателей) между узлами
n1.borrow_mut().left = Some(n2.clone());
n1.borrow_mut().right = Some(n3);
n2.borrow_mut().left = Some(n4);
n2.borrow_mut().right = Some(n5);
```
=== "C"
```c title="binary_tree.c"
/* Инициализация двоичного дерева */
// Инициализация узлов
TreeNode *n1 = newTreeNode(1);
TreeNode *n2 = newTreeNode(2);
TreeNode *n3 = newTreeNode(3);
TreeNode *n4 = newTreeNode(4);
TreeNode *n5 = newTreeNode(5);
// Построение ссылок (указателей) между узлами
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
```
=== "Kotlin"
```kotlin title="binary_tree.kt"
// Инициализация узлов
val n1 = TreeNode(1)
val n2 = TreeNode(2)
val n3 = TreeNode(3)
val n4 = TreeNode(4)
val n5 = TreeNode(5)
// Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "Ruby"
```ruby title="binary_tree.rb"
# Инициализация двоичного дерева
# Инициализация узлов
n1 = TreeNode.new(1)
n2 = TreeNode.new(2)
n3 = TreeNode.new(3)
n4 = TreeNode.new(4)
n5 = TreeNode.new(5)
# Построение ссылок (указателей) между узлами
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
??? pythontutor "Визуализация выполнения"
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%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
### Вставка и удаление узлов
Как и в связном списке, вставка и удаление узлов в двоичном дереве могут выполняться через изменение указателей. На рисунке ниже приведен пример.
![Вставка и удаление узлов в двоичном дереве](binary_tree.assets/binary_tree_add_remove.png)
=== "Python"
```python title="binary_tree.py"
# Вставка и удаление узлов
p = TreeNode(0)
# Вставить узел P между n1 -> n2
n1.left = p
p.left = n2
# Удалить узел P
n1.left = n2
```
=== "C++"
```cpp title="binary_tree.cpp"
/* Вставка и удаление узлов */
TreeNode* P = new TreeNode(0);
// Вставить узел P между n1 -> n2
n1->left = P;
P->left = n2;
// Удалить узел P
n1->left = n2;
// Освободить память
delete P;
```
=== "Java"
```java title="binary_tree.java"
TreeNode P = new TreeNode(0);
// Вставить узел P между n1 -> n2
n1.left = P;
P.left = n2;
// Удалить узел P
n1.left = n2;
```
=== "C#"
```csharp title="binary_tree.cs"
/* Вставка и удаление узлов */
TreeNode P = new(0);
// Вставить узел P между n1 -> n2
n1.left = P;
P.left = n2;
// Удалить узел P
n1.left = n2;
```
=== "Go"
```go title="binary_tree.go"
/* Вставка и удаление узлов */
// Вставить узел P между n1 -> n2
p := NewTreeNode(0)
n1.Left = p
p.Left = n2
// Удалить узел P
n1.Left = n2
```
=== "Swift"
```swift title="binary_tree.swift"
let P = TreeNode(x: 0)
// Вставить узел P между n1 -> n2
n1.left = P
P.left = n2
// Удалить узел P
n1.left = n2
```
=== "JS"
```javascript title="binary_tree.js"
/* Вставка и удаление узлов */
let P = new TreeNode(0);
// Вставить узел P между n1 -> n2
n1.left = P;
P.left = n2;
// Удалить узел P
n1.left = n2;
```
=== "TS"
```typescript title="binary_tree.ts"
/* Вставка и удаление узлов */
const P = new TreeNode(0);
// Вставить узел P между n1 -> n2
n1.left = P;
P.left = n2;
// Удалить узел P
n1.left = n2;
```
=== "Dart"
```dart title="binary_tree.dart"
/* Вставка и удаление узлов */
TreeNode P = new TreeNode(0);
// Вставить узел P между n1 -> n2
n1.left = P;
P.left = n2;
// Удалить узел P
n1.left = n2;
```
=== "Rust"
```rust title="binary_tree.rs"
let p = TreeNode::new(0);
// Вставить узел P между n1 -> n2
n1.borrow_mut().left = Some(p.clone());
p.borrow_mut().left = Some(n2.clone());
// Удалить узел p
n1.borrow_mut().left = Some(n2);
```
=== "C"
```c title="binary_tree.c"
/* Вставка и удаление узлов */
TreeNode *P = newTreeNode(0);
// Вставить узел P между n1 -> n2
n1->left = P;
P->left = n2;
// Удалить узел P
n1->left = n2;
// Освободить память
free(P);
```
=== "Kotlin"
```kotlin title="binary_tree.kt"
val P = TreeNode(0)
// Вставить узел P между n1 -> n2
n1.left = P
P.left = n2
// Удалить узел P
n1.left = n2
```
=== "Ruby"
```ruby title="binary_tree.rb"
# Вставка и удаление узлов
_p = TreeNode.new(0)
# Вставить узел _p между n1 -> n2
n1.left = _p
_p.left = n2
# Удалить узел
n1.left = n2
```
??? pythontutor "Визуализация выполнения"
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%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>.
![Идеальное двоичное дерево](binary_tree.assets/perfect_binary_tree.png)
### Полное двоичное дерево
Как показано на рисунке ниже, <u>полное двоичное дерево (complete binary tree)</u> допускает неполное заполнение только на самом нижнем уровне, причем узлы этого уровня должны непрерывно заполняться слева направо. Обрати внимание: идеальное двоичное дерево тоже является полным двоичным деревом.
![Полное двоичное дерево](binary_tree.assets/complete_binary_tree.png)
### Строгое двоичное дерево
Как показано на рисунке ниже, <u>строгое двоичное дерево (full binary tree)</u> имеет у всех нелистовых узлов ровно двух дочерних узлов.
![Строгое двоичное дерево](binary_tree.assets/full_binary_tree.png)
### Сбалансированное двоичное дерево
Как показано на рисунке ниже, в <u>сбалансированном двоичном дереве (balanced binary tree)</u> для любого узла абсолютное значение разности высот левого и правого поддеревьев не превышает 1 .
![Сбалансированное двоичное дерево](binary_tree.assets/balanced_binary_tree.png)
## Вырождение двоичного дерева
На рисунке ниже показаны идеальная структура двоичного дерева и вырожденная структура. Когда каждый уровень двоичного дерева полностью заполнен узлами, мы получаем "идеальное двоичное дерево"; когда же все узлы смещаются к одной стороне, двоичное дерево вырождается в "связный список".
- Идеальное двоичное дерево соответствует лучшему случаю и позволяет полностью раскрыть преимущества двоичного дерева с точки зрения "разделяй и властвуй".
- Связный список представляет противоположную крайность: все операции становятся линейными, а временная сложность деградирует до $O(n)$ .
![Лучший и худший случаи структуры двоичного дерева](binary_tree.assets/binary_tree_best_worst_cases.png)
Как показано в таблице ниже, в лучшем и худшем случаях число листовых узлов, общее число узлов, высота и другие характеристики двоичного дерева достигают максимума или минимума.
<p align="center"> Таблица <id> &nbsp; Лучший и худший случаи структуры двоичного дерева </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$ |
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

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>; он отражает идею "расширяться слой за слоем наружу".
![Обход двоичного дерева по уровням](binary_tree_traversal.assets/binary_tree_bfs.png)
### Код реализации
Обход в ширину обычно реализуется с помощью "очереди". Очередь подчиняется правилу "первым пришел - первым вышел", а обход в ширину подчиняется правилу "продвигаться по уровням", поэтому стоящая за ними идея согласована. Код реализации приведен ниже:
```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>; он отражает идею "сначала идти до конца, затем откатываться и продолжать".
На рисунке ниже показан принцип работы обхода двоичного дерева в глубину. **Обход в глубину похож на то, как будто мы обходим всю двоичную структуру по внешнему контуру** , и у каждого узла встречаем три позиции, соответствующие прямому, симметричному и обратному обходам.
![Прямой, симметричный и обратный обходы двоичного дерева поиска](binary_tree_traversal.assets/binary_tree_dfs.png)
### Код реализации
Поиск в глубину обычно реализуется через рекурсию:
```src
[file]{binary_tree_dfs}-[class]{}-[func]{post_order}
```
!!! tip
Поиск в глубину можно реализовать и итеративно; заинтересованные читатели могут изучить это самостоятельно.
На рисунках ниже показан рекурсивный процесс прямого обхода двоичного дерева. Его можно разделить на две противоположные части: "вход в рекурсию" и "возврат".
1. "Вход в рекурсию" означает запуск нового вызова функции; в этом процессе программа переходит к следующему узлу.
2. "Возврат" означает завершение вызова функции и возврат назад, то есть текущий узел уже полностью обработан.
=== "<1>"
![Рекурсивный процесс прямого обхода](binary_tree_traversal.assets/preorder_step1.png)
=== "<2>"
![preorder_step2](binary_tree_traversal.assets/preorder_step2.png)
=== "<3>"
![preorder_step3](binary_tree_traversal.assets/preorder_step3.png)
=== "<4>"
![preorder_step4](binary_tree_traversal.assets/preorder_step4.png)
=== "<5>"
![preorder_step5](binary_tree_traversal.assets/preorder_step5.png)
=== "<6>"
![preorder_step6](binary_tree_traversal.assets/preorder_step6.png)
=== "<7>"
![preorder_step7](binary_tree_traversal.assets/preorder_step7.png)
=== "<8>"
![preorder_step8](binary_tree_traversal.assets/preorder_step8.png)
=== "<9>"
![preorder_step9](binary_tree_traversal.assets/preorder_step9.png)
=== "<10>"
![preorder_step10](binary_tree_traversal.assets/preorder_step10.png)
=== "<11>"
![preorder_step11](binary_tree_traversal.assets/preorder_step11.png)
### Анализ сложности
- **Временная сложность равна $O(n)$** : все узлы посещаются по одному разу, поэтому требуется $O(n)$ времени.
- **Пространственная сложность равна $O(n)$** : в худшем случае, когда дерево вырождается в связный список, глубина рекурсии достигает $n$ , и система тратит $O(n)$ памяти на стек вызовов.
+9
View File
@@ -0,0 +1,9 @@
# Деревья
![Деревья](../assets/covers/chapter_tree.jpg)
!!! abstract
Высокое дерево полно жизни: мощные корни, густая крона и раскидистые ветви.
Оно наглядно показывает нам форму данных, построенную на принципе "разделяй и властвуй".
+54
View File
@@ -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$ .