Files
hello-algo/ru/chapters/chapter_07.md
2026-03-25 16:54:42 +08:00

1080 lines
76 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Деревья
![](ru/docs/assets/media/image210.jpeg){width="3.71875in" height="4.8125in"}
#### двоичные деревья
> *Двоичное (бинарное) дерево* -- это нелинейная структура данных, представля- ющая отношения между предками и потомками и отражающая логику «раз- деляй и властвуй». Подобно спискам, основным элементом двоичного дерева является узел, который содержит значение, ссылку на левый дочерний узел и ссылку на правый дочерний узел.
>
> class TreeNode:
>
> \"\"\" Класс узла двоичного дерева.\"\"\" def init (self, val: int):
>
> self.val: int = val \# Значение узла.
>
> self.left: TreeNode \| None = None \# Ссылка на левый дочерний узел. self.right: TreeNode \| None = None \# Ссылка на правый дочерний узел.
>
> Каждый узел имеет две ссылки (указателя), указывающие на левый и пра- вый дочерние узлы. Текущий узел называется родительским для этих двух до- черних узлов. Для заданного узла дерево, образованное его левым дочерним узлом и всеми его подузлами, называется левым поддеревом. Аналогично определяется правое поддерево.
>
> **Узлы, не имеющие дочерних узлов**, **называются листьями**, **все осталь- ные узлы содержат дочерние узлы и непустые поддеревья**. Если рассма- тривать узел 2 на рис. 7.1 как родительский, то его левым и правым дочерними узлами будут узел 4 и узел 5 соответственно. Левое поддерево -- это узел 4 и все узлы ниже него, а правое поддерево -- узел 5 и все узлы ниже него.
![](ru/docs/assets/media/image212.jpeg)
> **Рис. 7.1.** Родительский узел, дочерние узлы, поддеревья
1. **Основные понятия двоичного дерева**
> Основные понятия двоичного дерева изображены на рис. 7.2.
- **Корневой узел**: узел, находящийся на верхнем уровне дерева и не име- ющий родительского узла.
- **Листовой узел**: узел, не имеющий дочерних узлов, оба его указателя указывают на None.
- **Ребро**: отрезок, соединяющий два узла, т. е. ссылка (указатель) узла.
- **Уровень узла**: увеличивается сверху вниз, уровень корневого узла равен 1.
- **Степень узла**: количество дочерних узлов узла. В двоичном дереве сте- пень может быть 0, 1 или 2.
- **Высота двоичного дерева**: количество ребер от корневого узла до са- мого удаленного листового узла.
- **Глубина узла**: количество ребер от корневого узла до данного узла.
- **Высота узла**: количество ребер от самого удаленного листового узла до данного узла.
![](ru/docs/assets/media/image214.jpeg)
> **Рис. 7.2.** Основные понятия двоичного дерева
### Основные операции с двоичными деревьями
##### Инициализация двоичного дерева
> Подобно спискам, сначала инициализируются узлы, затем строятся ссылки (указатели).
>
> \# === File: 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
##### Вставка и удаление узлов
> Подобно спискам, в двоичном дереве вставку и удаление узлов можно выпол- нять путем изменения указателей. На рис. 7.3 приведен пример.
![](ru/docs/assets/media/image216.jpeg)
> **Рис. 7.3.** Вставка и удаление узлов в двоичном дереве
>
> \# === File: binary_tree.py === \# Вставка и удаление узлов.
>
> p = TreeNode(0)
>
> \# Вставка узла P между n1 и n2.
>
> n1.left = p p.left = n2
>
> \# Удаление узла P. n1.left = n2
### Основные типы двоичных деревьев
##### Идеальное двоичное дерево
> В идеальном двоичном дереве все уровни узлов полностью заполнены, см. рис. 7.4. В таком дереве степень листовых узлов равна 0, а степень всех осталь- ных узлов равна 2. Если высота дерева равна *h*, то общее количество узлов равно 2*h*+1 1, что представляет собой стандартное экспоненциальное соотношение, отражающее явление деления клеток, которое часто встречается в природе.
![](ru/docs/assets/media/image218.jpeg)
> **Рис. 7.4.** Идеальное двоичное дерево
##### Совершенное двоичное дерево
> В совершенном двоичном дереве (complete binary tree) заполнены не полно- стью только узлы на самом нижнем уровне, и они заполняются слева направо, см. рис. 7.5. Обратите внимание, что идеальное двоичное дерево также явля- ется полным.
![](ru/docs/assets/media/image220.jpeg)
> **Рис. 7.5.** Совершенное двоичное дерево
##### Полное двоичное дерево
> В полном двоичном дереве (full binary tree) все узлы, кроме листовых, имеют два дочерних узла, см. рис. 7.6.
![](ru/docs/assets/media/image222.jpeg)
> **Рис. 7.6.** Полное двоичное дерево
##### Сбалансированное двоичное дерево
> В сбалансированном двоичном дереве абсолютное значение разности высот левого и правого поддеревьев любого узла не превышает 1, см. рис. 7.7.
![](ru/docs/assets/media/image224.jpeg)
> **Рис. 7.7.** Сбалансированное двоичное дерево
### Вырождение двоичного дерева
> На рис. 7.8 изображена идеальная и вырожденная структура двоичного дерева. Когда каждый уровень двоичного дерева полностью заполнен узлами, оно ста- новится идеальным. Если все узлы смещены в одну сторону, двоичное дерево вырождается в связный список.
- Идеальное двоичное дерево является оптимальным случаем, позволя- ющим в полной мере использовать преимущество подхода «разделяй и властвуй».
- Связный список представляет собой другой крайний случай, когда все опе- рации становятся линейными, а временная сложность деградирует до *O*(*n*).
![](ru/docs/assets/media/image226.jpeg)
> **Рис. 7.8.** Идеальная и вырожденная структуры двоичного дерева
>
> Как показано в табл. 7.1, в идеальной и вырожденной структурах двоичного дерева количество листьев, общее количество узлов и высота достигают мак- симальных или минимальных значений.
>
> **Таблица 7.1.** Идеальная и вырожденная структуры двоичного дерева
>
> **Идеальное двоичное дерево Связный список**
>
> Количество узлов на уровне *i* 2*i*1 1
>
> Количество листьев в дереве высоты *h* 2*h* 1
>
> Общее количество узлов в дереве высоты *h*
>
> 2*h*+1 1 *h* + 1
>
> Высота дерева с *n* узлами log2(*n* + 1) 1 *n* 1
#### Обход двоичного дерева
> С физической точки зрения дерево является структурой данных, основанной на связном списке, поэтому его обход осуществляется последовательным до- ступом к узлам через указатели. Однако, будучи нелинейной структурой дан- ных, обход дерева сложнее, чем обход связного списка, и требует использова- ния алгоритмов поиска.
>
> Наиболее распространенные методы обхода двоичного дерева включают обход по уровням, прямой, симметричный и обратный обходы.
### Обход по уровням
> Обход по уровням осуществляется сверху вниз, выполняется последователь- ный обход двоичного дерева с посещением узлов на каждом уровне слева на- право, как показано на рис. 7.9.
>
> ![](ru/docs/assets/media/image228.jpeg)**Обход в ширину**
>
> **Обход по уровням**
>
> ![](ru/docs/assets/media/image229.png){width="8.0582895888014e-2in" height="8.0582895888014e-2in"}(узлы посещаются по точкам )
>
> **Рис. 7.9.** Обход двоичного дерева по уровням
>
> Обход по уровням по своей сути является обходом в ширину, также назы- ваемым поиском в ширину, который характеризуется постепенно расширяю- щимся кольцом от центра к периферии.
##### Код реализации
> Обход в ширину обычно реализуется с использованием очереди. Очередь сле- дует принципу «первый вошел -- первый вышел», а обход в ширину -- принципу
>
> «поэтапное продвижение», что делает их концептуально схожими. Ниже при- веден код реализации.
>
> \# === File: binary_tree_bfs.py ===
>
> def level_order(root: TreeNode \| None) -\> list\[int\]: \"\"\" Обход по уровням.\"\"\"
>
> \# Инициализация очереди, добавление корневого узла. queue: deque\[TreeNode\] = deque()
>
> queue.append(root)
>
> \# Инициализация списка для сохранения последовательности обхода. res = \[\]
>
> while queue:
>
> node: TreeNode = queue.popleft() \# Извлечение из очереди. res.append(node.val) \# Сохранение значения узла.
>
> if node.left is not None:
>
> queue.append(node.left) \# Добавление левого дочернего узла в оче-
>
> редь.
>
> if node.right is not None:
>
> queue.append(node.right) \# Добавление правого дочернего узла в очередь.
>
> return res
##### Анализ сложности
> **Временная сложность** *O*(*n*): каждый узел посещается один раз, что занимает
>
> *O*(*n*) времени выполнения, где *n* -- количество узлов.
>
> **Пространственная сложность** *O*(*n*): в худшем случае, т. е. в полном двоич- ном дереве, до достижения самого нижнего уровня в очереди может находить- ся одновременно (*n* + 1)/2 узлов, что занимает *O*(*n*) пространства.
### Прямой, симметричный и обратный обходы
> Прямой, симметричный и обратный обходы относятся к обходам в глубину, также называемым поиск в глубину, который характеризуется подходом «сна- чала до конца, затем возврат и продолжение».
>
> На рис. 7.10 демонстрируется принцип работы обхода в глубину для дво- ичного дерева. **Обход в глубину можно представить как обход двоичного дерева по периметру**, при этом на каждом узле встречаются три позиции, соответствующие прямому, симметричному и обратному обходам.
>
> ![](ru/docs/assets/media/image237.jpeg)
>
> **Рис. 7.10.** Прямой, симметричный и обратный обходы двоичного дерева
##### Код реализации
> Поиск в глубину обычно реализуется на основе рекурсии.
>
> \# === File: binary_tree_dfs.py === def pre_order(root: TreeNode \| None):
>
> \"\"\" Прямой обход.\"\"\"
>
> if root is None: return
>
> \# Приоритет посещения: корневой узел -\> левое поддерево -\> правое поддерево. res.append(root.val)
>
> pre_order(root=root.left) pre_order(root=root.right)
>
> def in_order(root: TreeNode \| None): \"\"\" Симметричный обход.\"\"\"
>
> if root is None: return
>
> \# Приоритет посещения: левое поддерево -\> корневой узел -\> правое поддерево. in_order(root=root.left)
>
> res.append(root.val) in_order(root=root.right)
>
> def post_order(root: TreeNode \| None): \"\"\" Обратный обход.\"\"\"
>
> if root is None: return
>
> \# Приоритет посещения: левое поддерево -\> правое поддерево -\> корневой узел. post_order(root=root.left)
>
> post_order(root=root.right) res.append(root.val)
>
> На рис. 7.11 демонстрируется рекурсивный процесс прямого обхода двоич- ного дерева, который можно разделить на два противоположных этапа: рекур- сия и возврат.
1. Рекурсия означает начало нового метода, в процессе которого програм- ма посещает следующий узел.
2. Возврат означает возвращение функции, что указывает на завершение посещения текущего узла.
![](ru/docs/assets/media/image246.jpeg)![](ru/docs/assets/media/image249.jpeg)
> **Рис. 7.11.** Рекурсивный процесс прямого обхода. Шаги 1--2
>
> ![](ru/docs/assets/media/image251.jpeg)
![](ru/docs/assets/media/image253.jpeg)![](ru/docs/assets/media/image256.jpeg)
> **Рис. 7.11.** *Продолжение*. Шаги 3--5
>
> ![](ru/docs/assets/media/image259.jpeg)
![](ru/docs/assets/media/image261.jpeg)![](ru/docs/assets/media/image263.jpeg)
> **Рис. 7.11.** *Продолжение*. Шаги 6--8
>
> ![](ru/docs/assets/media/image266.jpeg)
![](ru/docs/assets/media/image269.jpeg)![](ru/docs/assets/media/image271.jpeg)
> **Рис. 7.11.** *Окончание*. Шаги 9--11
##### 2. Анализ сложности
> **Временная сложность** *O*(*n*): все узлы посещаются один раз, что занимает *O*(*n*) времени.
>
> **Пространственная сложность** *O*(*n*): в худшем случае, когда дерево вырож- дается в список, глубина рекурсии достигает *n*, система занимает *O*(*n*) про- странства стека.
#### представление двоичного дерева с помощью массива
> При представлении в виде списка единицей хранения двоичного дерева яв- ляется узел TreeNode, а узлы соединяются между собой указателями. В преды- дущем разделе были рассмотрены основные операции с двоичным деревом, представленным в виде списка.
>
> Можно ли представить двоичное дерево с помощью массива? Ответ поло- жительный.
### Представление идеального двоичного дерева
> Сначала рассмотрим простой пример. Если дано идеальное двоичное дерево и все его узлы хранятся в массиве в порядке обхода по уровням, то каждому узлу соответствует уникальный индекс массива.
>
> На основе свойств обхода по уровням можно вывести формулу соответ- ствия между индексами родительского и дочерних узлов: **если индекс узла равен** *i*, **то индекс его левого дочернего узла равен** 2*i* + 1, **а правого** -- 2*i* + 2. На рис. 7.12 показаны отношения соответствия между индексами узлов.
![](ru/docs/assets/media/image273.jpeg)
> **Рис. 7.12.** Представление идеального двоичного дерева с помощью массива
>
> **Формула соответствия играет роль**, **аналогичную ссылкам (указате- лям) в списке**. Имея любой узел в массиве, можно с помощью формулы полу- чить доступ к его левому и правому дочерним узлам.
### Представление произвольного двоичного дерева
> Идеальное двоичное дерево является частным случаем. Обычно на средних уровнях двоичного дерева присутствует много пустых значений None. Но по- следовательность обхода по уровням не содержит этих None, поэтому невоз- можно по этой последовательности определить количество и расположение пустых значений. **Это означает**, **что существует множество структур дво- ичных деревьев**, **соответствующих данной последовательности обхода по уровням**.
>
> Для такого неидеального двоичного дерева вышеописанный метод пред- ставления с помощью массива уже не работает, см. рис. 7.13.
![](ru/docs/assets/media/image275.jpeg)
> **Рис. 7.13.** Для одной последовательности обхода по уровням существует несколько возможных вариантов двоичного дерева
>
> Для решения этой проблемы **можно явно записать все значения** None **в последовательности обхода по уровням**. После такой обработки последо- вательность обхода по уровням уже может однозначно представлять двоич- ное дерево, как показано на рис. 7.14. Ниже приведен пример кода.
>
> \# Представление двоичного дерева с помощью массива. \# Использование None для обозначения пустых мест.
>
> tree = \[1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15\]
>
> ![](ru/docs/assets/media/image277.jpeg)
>
> **Рис. 7.14.** Представление произвольного двоичного дерева с помощью массива
>
> Стоит отметить, что **совершенное двоичное дерево очень удобно пред- ставлять с помощью массива**. Вспоминая определение совершенного дво- ичного дерева, None появляются только на самом нижнем уровне и в правой части, поэтому **все значения** None **обязательно находятся в конце последо- вательности обхода по уровням**.
>
> Это означает, что при использовании массива для представления совершен- ного двоичного дерева можно опустить хранение всех None, что очень удобно. На рис. 7.15 приведен пример такого представления.
![](ru/docs/assets/media/image279.jpeg)
> **Рис. 7.15.** Представление совершенного двоичного дерева с помощью массива
>
> В коде ниже реализуется двоичное дерево, основанное на представлении с помощью массива, включая следующие операции.
- Для заданного узла получение его значения, левого и правого дочернего узла, родительского узла.
- Получение последовательностей обхода в прямом, симметричном, об- ратном порядке и в порядке обхода по уровням.
> \# === File: array_binary_tree.py === class ArrayBinaryTree:
>
> \"\"\" Класс двоичного дерева, представленного с помощью массива.\"\"\"
>
> def init (self, arr: list\[int \| None\]): \"\"\" Конструктор.\"\"\"
>
> self.\_tree = list(arr)
>
> def size(self):
>
> \"\"\" Вместимость списка.\"\"\" return len(self.\_tree)
>
> def val(self, i: int) -\> int \| None:
>
> \"\"\" Получение значения узла с индексом i.\"\"\"
>
> \# Если индекс выходит за границы, возвращается None, \# обозначающее пустое место.
>
> if i \< 0 or i \>= self.size(): return None
>
> return self.\_tree\[i\]
>
> def left(self, i: int) -\> int \| None:
>
> \"\"\" Получение индекса левого дочернего узла для узла с индексом i.\"\"\" return 2 \* i + 1
>
> def right(self, i: int) -\> int \| None:
>
> \"\"\" Получение индекса правого дочернего узла для узла с индексом i.\"\"\" return 2 \* i + 2
>
> def parent(self, i: int) -\> int \| None:
>
> \"\"\" Получение индекса родительского узла для узла с индексом i.\"\"\" return (i - 1) // 2
>
> def level_order(self) -\> list\[int\]: \"\"\" Обход по уровням.\"\"\" self.res = \[\]
>
> \# Прямой обход массива.
>
> for i in range(self.size()):
>
> if self.val(i) is not None: self.res.append(self.val(i))
>
> return self.res
>
> def dfs(self, i: int, order: str): \"\"\" Обход в глубину.\"\"\"
>
> if self.val(i) is None: return
>
> \# Прямой обход.
>
> if order == \"pre\": self.res.append(self.val(i)) self.dfs(self.left(i), order)
>
> \# Симметричный обход. if order == \"in\":
>
> self.res.append(self.val(i)) self.dfs(self.right(i), order)
>
> \# Обратный обход. if order == \"post\":
>
> self.res.append(self.val(i))
>
> def pre_order(self) -\> list\[int\]: \"\"\" Прямой обход.\"\"\"
>
> self.res = \[\] self.dfs(0, order=\"pre\") return self.res
>
> def in_order(self) -\> list\[int\]: \"\"\" Симметричный обход.\"\"\" self.res = \[\]
>
> self.dfs(0, order=\"in\") return self.res
>
> def post_order(self) -\> list\[int\]: \"\"\" Обратный обход.\"\"\" self.res = \[\]
>
> self.dfs(0, order=\"post\") return self.res
### Преимущества и ограничения
> Представление двоичного дерева с помощью массива имеет следующие пре- имущества:
- массив хранится в непрерывной области памяти, что хорошо для кеши- рования. Скорость доступа и обхода достаточно высока;
- не требуется хранение указателей, что экономит пространство;
- позволяет выполнять произвольный доступ к узлам.
> Однако представление с помощью массива имеет и некоторые ограничения.
- Хранение в массиве требует непрерывной области памяти, поэтому не подходит для хранения деревьев с очень большим объемом данных.
- Добавление и удаление узлов требует выполнения операций вставки и удаления в массиве, которые менее эффективны.
- Когда в двоичном дереве содержится много значений None, доля данных узлов в массиве низка, что приводит к низкой эффективности использо- вания пространства.
#### двоичное дерево поиска
> *Двоичное дерево поиска* удовлетворяет следующим условиям, см. рис. 7.16:
1) для корневого узла все значения узлов в левом поддереве \< значение корневого узла \< все значения узлов в правом поддереве;
2) левое и правое поддеревья любого узла также являются двоичными де- ревьями поиска, т. е. удовлетворяют условию 1.
![](ru/docs/assets/media/image281.jpeg)
> **Рис. 7.16.** Двоичное дерево поиска
### Операции с двоичным деревом поиска
> Мы инкапсулируем двоичное дерево поиска в класс BinarySearchTree и объявля- ем переменную-член root, указывающую на корневой узел дерева.
##### Поиск узла
> Для заданного значения целевого узла num можно выполнить поиск, основы- ваясь на свойствах двоичного дерева поиска. Мы объявляем текущий узел cur, начиная с корневого узла дерева root, и в цикле сравниваем значения узлов cur.val и num, как показано на рис. 7.17.
- Если cur.val \< num, значит целевой узел находится в правом поддереве
> cur, поэтому выполняется переход cur = cur.right.
- Если cur.val \> num, значит целевой узел находится в левом поддереве cur, поэтому выполняется переход cur = cur.left.
- Если cur.val = num, значит целевой узел найден, выполняется выход из цикла и возврат этого узла.
> ![](ru/docs/assets/media/image283.jpeg)
![](ru/docs/assets/media/image285.jpeg)![](ru/docs/assets/media/image287.jpeg)
> **Рис. 7.17.** Пример поиска узла в двоичном дереве поиска. Шаги 1--3
>
> ![](ru/docs/assets/media/image289.jpeg)
>
> **Рис. 7.17.** *Окончание*. Шаг 4
>
> Операция поиска в двоичном дереве поиска аналогична принципу работы алго- ритма двоичного поиска, который исключает половину случаев на каждой итера- ции. Максимальное количество циклов равно высоте двоичного дерева, и при сба- лансированном дереве требуется *O*(log *n*) времени. Ниже приведен пример кода.
>
> \# === File: binary_search_tree.py ===
>
> def search(self, num: int) -\> TreeNode \| None: \"\"\" Поиск узла.\"\"\"
>
> cur = self.\_root
>
> \# Циклический поиск, выход после прохождения листового узла. while cur is not None:
>
> \# Целевой узел в правом поддереве cur. if cur.val \< num:
>
> cur = cur.right
>
> \# Целевой узел в левом поддереве cur. elif cur.val \> num:
>
> cur = cur.left
>
> \# Найден целевой узел, выход из цикла.
>
> else:
>
> break return cur
##### Вставка узла
> Нужно вставить новый элемент num и при этом сохранить свойство двоичного дерева поиска: левое поддерево \< корневой узел \< правое поддерево. Процесс вставки показан на рис. 7.18.
1. **Поиск позиции для вставки**: аналогично операции поиска, начиная с корневого узла, в цикле выполняется поиск вниз по дереву в зависимо- сти от соотношения значений текущего узла и num, пока не будет пройден листовой узел (достигнуто None), после чего цикл завершается.
2. **Вставка узла в найденную позицию**: инициализация узла num и раз- мещение его в позиции None.
![](ru/docs/assets/media/image291.jpeg)
> **Рис. 7.18.** Вставка узла в двоичное дерево поиска
>
> В коде реализации следует обратить внимание на следующие моменты.
- В двоичном дереве поиска не допускается наличие дублирующихся уз- лов, иначе будет нарушено его определение. Поэтому, если узел, который нужно вставить, уже существует в дереве, вставка не выполняется, и про- исходит возврат.
- Для выполнения вставки узла необходимо использовать узел pre, чтобы сохранить узел предыдущей итерации цикла. Таким образом, при дости- жении None можно получить родительский узел и завершить операцию вставки узла.
> \# === File: binary_search_tree.py === def insert(self, num: int):
>
> \"\"\" Вставка узла.\"\"\"
>
> \# Если дерево пусто, инициализация корневого узла. if self.\_root is None:
>
> self.\_root = TreeNode(num) return
>
> \# Циклический поиск, выход после прохождения листового узла. cur, pre = self.\_root, None
>
> while cur is not None:
>
> \# Найден дублирующий узел, возврат. if cur.val == num:
>
> return pre = cur
>
> \# Позиция для вставки в правом поддереве cur. if cur.val \< num:
>
> cur = cur.right
>
> \# Позиция для вставки в левом поддереве cur.
>
> else:
>
> cur = cur.left \# Вставка узла.
>
> node = TreeNode(num) if pre.val \< num:
>
> pre.right = node
>
> else:
>
> pre.left = node
>
> Как и в случае поиска узла, вставка узла выполняется за время *O*(log *n*).
##### Удаление узла
> Сначала в двоичном дереве выполняется поиск целевого узла, после чего он удаляется. Как и при вставке узла, необходимо гарантировать, что после за- вершения операции удаления сохраняется свойство двоичного дерева поиска: левое поддерево \< корневой узел \< правое поддерево. Поэтому, в зависимости от количества дочерних узлов целевого узла (0, 1 или 2), выполняются соответ- ствующие операции его удаления.
>
> Если степень удаляемого узла равна 0, значит он является листовым, и его можно удалить напрямую, см. рис. 7.19.
![](ru/docs/assets/media/image293.jpeg)
> **Рис. 7.19.** Удаление узла в двоичном дереве поиска (степень 0)
>
> Если степень удаляемого узла равна 1, его можно заменить дочерним узлом, см. рис. 7.20.
![](ru/docs/assets/media/image295.jpeg) **Удаление узла из двоичного дерева поиска**
1. *Найти узел* **cur**,
> *подлежащий удалению*
>
> *У узла* **cur** *количество дочерних узлов = 1*
2. *Заменить узел* **cur** *его дочерним узлом*
> *Выполнить* **pre**.left = **cur**.right
>
> **Рис. 7.20.** Удаление узла в двоичном дереве поиска (степень 1)
>
> Если степень удаляемого узла равна 2, его нельзя удалить напрямую, и не- обходимо заменить его другим узлом. Согласно свойству двоичного дерева по- иска левое поддерево \< корневой узел \< правое поддерево, **этот узел может быть минимальным узлом правого поддерева или максимальным узлом левого поддерева**.
>
> Предположим, что мы выбираем минимальный узел правого поддерева (следующий узел при симметричном обходе), тогда процесс удаления будет следующим (см. рис. 7.21):
1) найти следующий узел в последовательности симметричного обхода для узла, который необходимо удалить, и обозначить его как tmp;
2) заменить значение удаляемого узла значением tmp и рекурсивно уда- лить узел tmp из дерева.
![](ru/docs/assets/media/image297.jpeg)
> **Рис. 7.21.** Удаление узла в двоичном дереве поиска (степень 2). Шаг 1
>
> ![](ru/docs/assets/media/image299.jpeg)
![](ru/docs/assets/media/image301.jpeg)
> **Рис. 7.21.** *Продолжение*.. Шаг 2--3
>
> ![](ru/docs/assets/media/image303.jpeg)
>
> **Рис. 7.21.** *Окончание*. Шаг 4
>
> Операция удаления узла также выполняется за время *O*(log *n*). При этом по- иск удаляемого узла требует времени *O*(log *n*) и получение следующего узла при симметричном обходе также требует времени *O*(log *n*). Ниже приведен пример кода.
>
> \# === File: binary_search_tree.py === def remove(self, num: int):
>
> \"\"\" Удаление узла.\"\"\"
>
> \# Если дерево пусто, немедленный возврат. if self.\_root is None:
>
> return
>
> \# Циклический поиск, выход после прохождения листового узла. cur, pre = self.\_root, None
>
> while cur is not None:
>
> \# Найден узел для удаления, выход из цикла. if cur.val == num:
>
> break pre = cur
>
> \# Узел для удаления находится в правом поддереве cur. if cur.val \< num:
>
> cur = cur.right
>
> \# Узел для удаления находится в левом поддереве cur.
>
> else:
>
> cur = cur.left
>
> \# Если узел для удаления не найден, возврат. if cur is None:
>
> return
>
> \# Количество дочерних узлов = 0 или 1.
>
> if cur.left is None or cur.right is None:
>
> \# Если количество дочерних узлов = 0 / 1, child = null / этот дочерний узел. child = cur.left or cur.right
>
> \# Удаление узла cur. if cur != self.\_root:
>
> if pre.left == cur: pre.left = child
>
> else:
>
> pre.right = child
>
> else:
>
> \# Если удаляемый узел - корень, переназначаем корень. self.\_root = child
>
> \# Количество дочерних узлов = 2.
>
> else:
>
> \# Получение следующего узла при симметричном обходе для cur. tmp: TreeNode = cur.right
>
> while tmp.left is not None: tmp = tmp.left
>
> \# Рекурсивное удаление узла tmp. self.remove(tmp.val)
>
> \# Замена cur на tmp cur.val = tmp.val
##### 4. Упорядоченность симметричного обхода
> Симметричный обход двоичного дерева следует порядку лево → корень → пра- во, а двоичное дерево поиска удовлетворяет соотношению левый узел \< корне- вой узел \< правый узел, см. рис. 7.22.
>
> Это означает, что при симметричном обходе двоичного дерева поиска всег- да сначала будет посещаться следующий минимальный узел, что приводит к важному свойству: **последовательность симметричного обхода двоично- го дерева поиска является возрастающей**.
>
> Используя свойство возрастающей последовательности симметричного обхода, можно получить упорядоченные данные в двоичном дереве поиска за время *O*(*n*) без необходимости в дополнительных операциях сортировки, что очень эффективно.
>
> ![](ru/docs/assets/media/image306.jpeg)
>
> **Рис. 7.22.** Симметричный обход двоичного дерева поиска
### Эффективность двоичного дерева поиска
> Для заданного набора данных можно использовать для его хранения массив или двоичное дерево поиска. Как показано в табл. 7.2, временная сложность операций в двоичном дереве поиска имеет логарифмический порядок, что обеспечивает стабильную и высокую производительность. Только в случае ча- стого добавления и редкого поиска и удаления данных массив будет более эф- фективен, чем двоичное дерево поиска.
>
> **Таблица 7.2.** Сравнение эффективности массива и дерева поиска
+------------------------+------------------------------+------------------------------+
| > **Операция** | > **Неупорядоченный массив** | > **Двоичное дерево поиска** |
+========================+==============================+==============================+
| > Поиск элемента | > *O*(*n*) | > *O*(log *n*) |
+------------------------+------------------------------+------------------------------+
| > Вставка элемента | > *O*(1) | > *O*(log *n*) |
+------------------------+------------------------------+------------------------------+
| > Удаление элемента | > *O*(*n*) | > *O*(log *n*) |
+------------------------+------------------------------+------------------------------+
| > В идеальных условиях | > двоичное дерево поиска | > является сбалансирован- |
+------------------------+------------------------------+------------------------------+
> ным, что позволяет находить любой узел за log *n* итераций.
>
> Однако, если в двоичном дереве поиска постоянно добавлять и удалять узлы, это может привести к его вырождению в список, как показано на рис. 7.23. Тог- да временная сложность различных операций также деградирует до *O*(*n*).
>
> ![](ru/docs/assets/media/image309.jpeg)
>
> **Рис. 7.23.** Вырождение двоичного дерева поиска
### Типичные сценарии применения двоичного дерева поиска
- Используется в качестве многоуровневого индекса в системах для эф- фективного поиска, вставки и удаления.
- Служит базовой структурой данных для некоторых алгоритмов поиска.
- Применяется для хранения потока данных для поддержания его упоря- доченного состояния.
1. **АВЛ-дерево\***
> В разделе «Двоичное дерево поиска» упоминалось, что после многократ- ных операций вставки и удаления двоичное дерево поиска может выродит- ся в список. В таких случаях временная сложность всех операций ухудшается с *O*(log *n*) до *O*(*n*).
>
> На рис. 7.24 приведен пример, когда после двух операций удаления узлов двоичное дерево поиска вырождается в список.
>
> В другом примере после вставки двух узлов в идеальное двоичное дерево, показанное на рис. 7.25, дерево сильно наклоняется влево, и временная слож- ность операций поиска также ухудшается.
>
> В 1962 году советские математики Г. М. Адельсон-Вельский и Е. М. Ландис в статье «Один алгоритм организации информации» предложили структуру АВЛ- дерева. В статье подробно описывается серия операций, которые гарантируют, что после постоянного добавления и удаления узлов АВЛ-дерево не деградиру- ет, что позволяет поддерживать временную сложность различных операций на уровне *O*(log *n*). Иными словами, в сценариях, требующих частых операций до- бавления, удаления, поиска и изменения, АВЛ-дерево обеспечивает высокую эф- фективность операций с данными и имеет значительную прикладную ценность.
>
> ![](ru/docs/assets/media/image311.jpeg)
>
> **Рис. 7.24.** Вырождение АВЛ-дерева после удаления узлов
![](ru/docs/assets/media/image313.jpeg)
> **Рис. 7.25.** Вырождение АВЛ-дерева после вставки узлов
### Основные понятия АВЛ-дерева
> *АВЛ-дерево* является одновременно и двоичным деревом поиска, и сбалан- сированным двоичным деревом, удовлетворяя всем свойствам этих двух типов деревьев. Таким образом, оно представляет собой сбалансированное двоичное дерево поиска.
##### Высота узла
> Поскольку операции с АВЛ-деревом требуют получения высоты узла, необхо- димо добавить в класс узла переменную height.
>
> 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 \# Ссылка на правый дочерний узел.
>
> Высота узла определяется как расстояние от данного узла до самого удаленно- го листа, т. е. количество ребер, через которые проходит этот путь. Следует особо отметить, что высота листа равна 0, а высота пустого узла равна --1. Нам понадо- бятся две вспомогательные функции для получения и обновления высоты узла.
>
> \# === File: avl_tree.py ===
>
> def height(self, node: TreeNode \| None) -\> int: \"\"\"Получение высоты узла.\"\"\"
>
> \# Высота пустого узла равна -1, высота листа равна 0.
>
> if node is not None:
>
> return node.height return -1
>
> def update_height(self, node: TreeNode \| None): \"\"\"Обновление высоты узла.\"\"\"
>
> \# Высота узла равна высоте самого высокого поддерева + 1.
>
> node.height = max(\[self.height(node.left), self.height(node.right)\]) + 1
##### Фактор баланса узла
> Фактор баланса узла определяется как высота левого поддерева узла минус высота правого поддерева, при этом фактор баланса пустого узла равен 0. Мы обернем функцию получения фактора баланса узла в отдельную функцию для удобства дальнейшего использования.
>
> \# === File: avl_tree.py ===
>
> def balance_factor(self, node: TreeNode \| None) -\> int: \"\"\"Получение балансировочного фактора.\"\"\"
>
> \# Фактор баланса пустого узла равен 0.
>
> if node is None:
>
> return 0
>
> \# Фактор баланса узла = высота левого поддерева - высота правого поддерева. return self.height(node.left) - self.height(node.right)
### Вращение в АВЛ-дереве
> Особенностью АВЛ-дерева является операция вращения, которая позволяет восстановить баланс узла, не влияя на порядок обхода двоичного дерева. Ины- ми словами, **вращение поворота сохраняет свойства двоичного дерева поиска и делает дерево снова сбалансированным двоичным деревом**.
>
> Узлы с абсолютным значением фактора баланса \> 1 называются несбалан- сированными узлами. В зависимости от типа несбалансированности узла опе- рации вращения делятся на четыре типа: правое; левое; сначала правое, затем левое; сначала левое, затем правое. Рассмотрим их подробнее.
##### Правое вращение
> На рис. 7.26 ниже узла указан фактор баланса. Если идти снизу вверх, в дво- ичном дереве первым несбалансированным узлом является узел 3. Рассмо- трим поддерево с этим узлом в качестве корня: обозначим этот узел как node, а его левый дочерний узел как child и выполним операцию правого вращения. После завершения операции поддерево восстанавливает баланс и сохраняет свойства двоичного дерева поиска.
![](ru/docs/assets/media/image315.jpeg)![](ru/docs/assets/media/image317.jpeg)
> **Рис. 7.26.** Этапы правого вращения. Шаги 1--2
>
> ![](ru/docs/assets/media/image319.jpeg)
![](ru/docs/assets/media/image321.jpeg)
> **Рис. 7.26.** *Окончание*.. Шаги 3--4
>
> Если у узла child есть правый дочерний узел (обозначим его как grand_child), необходимо добавить в правое вращение еще один шаг: сделать grand_child ле- вым дочерним узлом для node.
>
> Правое вращение -- это образное выражение, фактически оно реализуется путем изменения указателей узлов, как показано в приведенном ниже коде.
>
> ![](ru/docs/assets/media/image323.jpeg)
>
> **Рис. 7.27.** Правое вращение с grand_child
>
> \# === File: avl_tree.py ===
>
> def right_rotate(self, node: TreeNode \| None) -\> TreeNode \| None: \"\"\"Правое вращение.\"\"\"
>
> child = node.left grand_child = child.right
>
> \# С использованием child в качестве опорной точки выполнить правое вращение node. child.right = node
>
> node.left = grand_child
>
> \# Обновление высоты узлов. self.update_height(node) self.update_height(child)
>
> \# Возврат корневого узла поддерева после вращения. return child
##### Левое вращение
> Соответственно, если рассмотреть зеркальное отражение вышеупомянутого несбалансированного двоичного дерева, необходимо выполнить операцию левого вращения, как показано на рис. 7.28.
![](ru/docs/assets/media/image325.jpeg)
> **Рис. 7.28.** Левое вращение
>
> Аналогично, если у узла child есть левый дочерний узел (обозначим его как grand_child), необходимо добавить в левое вращение еще один шаг: сделать grand_child правым дочерним узлом для node, как показано на рис. 7.29.
![](ru/docs/assets/media/image327.jpeg)
> **Рис. 7.29.** Левое вращение с grand_child
>
> Можно заметить, **что правое и левое вращение логически являются зеркально симметричными, и они решают две симметричные ситуации несбалансированности**. Поэтому достаточно заменить в коде реализации правого вращения все left на right и все right на left, чтобы получить код реа- лизации левого вращения.
>
> \# === File: avl_tree.py ===
>
> def left_rotate(self, node: TreeNode \| None) -\> TreeNode \| None: \"\"\"Левый поворот.\"\"\"
>
> child = node.right grand_child = child.left
>
> \# С использованием child в качестве опорной точки выполнить левый поворот node. child.left = node
>
> node.right = grand_child
>
> \# Обновление высоты узлов. self.update_height(node) self.update_height(child)
>
> \# Возврат корневого узла поддерева после поворота. return child
##### Сначала левое, затем правое вращение
> Для несбалансированного узла 3 на рис. 7.30 использование только левого или правого вращения не позволяет восстановить баланс поддерева. В этом случае необходимо сначала выполнить левое вращение для child, а затем правое вра- щение для node.
>
> ![](ru/docs/assets/media/image329.jpeg)
>
> **Рис. 7.30.** Сначала левое, затем правое вращение
##### Сначала правое, затем левое вращение
> Для зеркальной ситуации вышеупомянутого разбалансированного двоичного дерева необходимо сначала выполнить правое вращение для child, а затем ле- вое вращение для node, как показано на рис. 7.31.
![](ru/docs/assets/media/image331.jpeg)
> **Рис. 7.31.** Сначала правое, затем левое вращение
##### Выбор типа вращения
> На рис. 7.32 изображено четыре типа несбалансированности, соответствую- щие вышеописанным случаям, для которых необходимо применять операции: правого вращения; сначала левого, затем правого вращения; сначала правого, затем левого вращения; левого вращения соответственно.
>
> Из табл. 7.3 видно, что для определения того, к какому случаю из рис. 7.32 относится несбалансированный узел, используется фактор баланса узла и знак фактора баланса дочернего узла с большей высотой.
>
> ![](ru/docs/assets/media/image333.jpeg)
>
> **Рис. 7.32.** Четыре типа вращений в АВЛ-дереве
+-------------------------------------------------+----------------------+----------------------------------------+
| > **Фактор баланса несбаланси- рованного узла** | > **Фактор баланса** | > **Рекомендуемый метод вращения** |
+=================================================+======================+========================================+
| > \> 1 (левостороннее дерево) | > ≥ 0 | > Правое вращение |
+-------------------------------------------------+----------------------+----------------------------------------+
| > \> 1 (левостороннее дерево) | > \< 0 | > Сначала левое, затем правое вращение |
+-------------------------------------------------+----------------------+----------------------------------------+
| > \< -1 (правостороннее дерево) | > ≤ 0 | > Левое вращение |
+-------------------------------------------------+----------------------+----------------------------------------+
| > \< -1 (правостороннее дерево) | > \> 0 | > Сначала правое, затем левое вращение |
+-------------------------------------------------+----------------------+----------------------------------------+
> **дочернего узла**
>
> Для удобства использования операции вращения инкапсулированы в функцию. **С помощью этой функции можно выполнять вращения для различных случаев несбалансированности узла**. Ниже приведен код ре- ализации.
>
> \# === File: avl_tree.py ===
>
> def rotate(self, node: TreeNode \| None) -\> TreeNode \| None:
>
> \"\"\" Выполнение операции вращения для восстановления баланса поддерева.\"\"\" \# Получение фактора баланса узла node.
>
> balance_factor = self.balance_factor(node) \# Левостороннее дерево.
>
> if balance_factor \> 1:
>
> if self.balance_factor(node.left) \>= 0: \# Правое вращение.
>
> return self.right_rotate(node)
>
> else:
>
> \# Сначала левое, затем правое вращение. node.left = self.left_rotate(node.left) return self.right_rotate(node)
>
> \# Правостороннее дерево. elif balance_factor \< -1:
>
> if self.balance_factor(node.right) \<= 0: \# Левое вращение.
>
> return self.left_rotate(node)
>
> else:
>
> \# Сначала правое, затем левое вращение. node.right = self.right_rotate(node.right) return self.left_rotate(node)
>
> \# Сбалансированное дерево, вращение не требуется. return node
### Основные операции с АВЛ-деревом
##### Вставка узла
> Операция вставки узла в АВЛ-дерево в основном схожа с двоичным деревом поиска. Единственное отличие заключается в том, что после вставки узла в АВЛ-дерево на пути от этого узла к корню могут возникнуть несбалансиро- ванные узлы. Поэтому*,* **начиная с этого узла**, **необходимо выполнять вра- щения снизу вверх**, **чтобы восстановить баланс всех несбалансирован- ных узлов**. Ниже приведен код реализации.
>
> \# === File: avl_tree.py === def insert(self, val):
>
> \"\"\" Вставка узла.\"\"\"
>
> self.\_root = self.insert_helper(self.\_root, val)
>
> def insert_helper(self, node: TreeNode \| None, val: int) -\> TreeNode: \"\"\" Рекурсивная вставка узла (вспомогательный метод).\"\"\"
>
> if node is None:
>
> return TreeNode(val)
>
> \# 1. Поиск позиции для вставки и вставка узла. if val \< node.val:
>
> node.left = self.insert_helper(node.left, val) elif val \> node.val:
>
> node.right = self.insert_helper(node.right, val)
>
> else:
>
> \# Повторяющийся узел не вставляется, возвращается напрямую. return node
>
> \# Обновление высоты узла. self.update_height(node)
>
> \# 2. Выполнение операции вращения для восстановления баланса поддерева. return self.rotate(node)
##### Удаление узла
> Для удаления узла можно также взять метод удаления узла в двоичном дереве поиска и добавить вращения при движении снизу вверх, чтобы восстановить баланс всех несбалансированных узлов. Ниже приведен код реализации.
>
> \# === File: avl_tree.py === def remove(self, val: int):
>
> \"\"\" Удаление узла.\"\"\"
>
> self.\_root = self.remove_helper(self.\_root, val)
>
> def remove_helper(self, node: TreeNode \| None, val: int) -\> TreeNode \| None: \"\"\" Рекурсивное удаление узла (вспомогательный метод).\"\"\"
>
> if node is None:
>
> return None
>
> \# 1. Поиск узла и его удаление. if val \< node.val:
>
> node.left = self.remove_helper(node.left, val) elif val \> node.val:
>
> node.right = self.remove_helper(node.right, val)
>
> else:
>
> if node.left is None or node.right is None: child = node.left or node.right
>
> \# Количество подузлов = 0, узел node удаляется напрямую и выполня-
>
> ется возврат.
>
> if child is None:
>
> return None
>
> \# Количество подузлов = 1, узел node удаляется напрямую.
>
> else:
>
> node = child
>
> else:
>
> \# Количество подузлов = 2, следующий узел в порядке обхода удаляет- ся, а текущий узел заменяется этим узлом.
>
> temp = node.right
>
> while temp.left is not None: temp = temp.left
>
> node.right = self.remove_helper(node.right, temp.val) node.val = temp.val
>
> \# Обновление высоты узла. self.update_height(node)
>
> \# 2. Выполнение операции вращения для восстановления баланса поддерева. return self.rotate(node)
##### Поиск узла
> Операция поиска узла в АВЛ-дереве идентична поиску в двоичном дереве по- иска, поэтому здесь повторно не рассматривается.
### Типичные сценарии применения АВЛ-дерева
- Организация и хранение больших объемов данных подходит для сцена- риев с частыми поисками и редкими вставками и удалениями.
- Используется для построения индексных систем в базах данных.
- Красно-черное дерево также является распространенным видом сбалан- сированного двоичного дерева поиска. По сравнению с АВЛ-деревом ус- ловия баланса в красно-черном дереве более мягкие, что требует меньше вращений при вставке и удалении узлов и обеспечивает более высокую среднюю эффективность этих операций.
#### резюме
##### Ключевые моменты
- Двоичное (бинарное) дерево -- это нелинейная структура данных, отра- жающая логику «разделяй и властвуй». Каждый узел двоичного дерева содержит значение и два указателя, указывающих на его левый и правый дочерние узлы соответственно.
- Для любого узла в двоичном дереве его левый (правый) дочерний узел и об- разуемое им дерево называются левым (правым) поддеревом этого узла.
- Связанные с двоичным деревом понятия включают корневой узел, ли- стовой узел, уровень, степень, ребро, высоту, глубину и др.
- Инициализация двоичного дерева, вставка и удаление узлов аналогичны методам работы со списками.
- К распространенным типам двоичных деревьев относятся идеальное двоичное дерево, совершенное двоичное дерево, полное двоичное де- рево и сбалансированное двоичное дерево. Идеальное двоичное дерево является наиболее желаемым состоянием, а список -- наихудшим состо- янием после вырождения.
- Двоичное дерево может быть представлено массивом, в котором значе- ния узлов и пустые места располагаются в порядке обхода по уровням, а указатели реализуются на основе индексации между родительскими и дочерними узлами.
- Обход двоичного дерева по уровням является методом поиска в шири- ну, который отражает способ обхода по кругам, расширяющимся наружу и обычно реализуется с помощью очереди.
- Прямой, симметричный и обратный обходы относятся к методам поиска в глубину. Они демонстрируют способ обхода «сначала до конца, затем воз- врат и продолжение», обычно реализуемый с использованием рекурсии.
- Двоичное дерево поиска представляет собой эффективную структуру дан- ных для поиска элементов, где временная сложность операций поиска, вставки и удаления составляет *O*(log *n*). Когда двоичное дерево поиска вы- рождается в список, временная сложность всех операций ухудшается до *O*(*n*).
- АВЛ-дерево, также известное как сбалансированное двоичное дерево поиска, поддерживает выполнение балансировки дерева после вставки и удаления узлов с помощью операций вращения.
> 7.6. Резюме ❖ **211**
- Операции вращения в АВЛ-дереве включают: правое вращение; левое вращение; сначала левое, затем правое вращение; сначала правое, затем левое вращение. После вставки или удаления узлов АВЛ-дерево выпол- няет операции вращения снизу вверх, чтобы восстановить баланс.
##### Вопросы и ответы
> **Вопрос**. Для двоичного дерева с единственным узлом высота дерева и глубина корневого узла равны 0?
>
> **Ответ**. Да, поскольку высота и глубина обычно определяются как количе- ство пройденных ребер.
>
> **Вопрос**. Вставка и удаление в двоичном дереве обычно выполняются с по- мощью набора операций. Что подразумевается под набором операций? Мож- но ли это понимать как освобождение ресурсов дочерних узлов?
>
> **Ответ**. Возьмем, к примеру, двоичное дерево поиска: операция удаления узла требует обработки трех различных случаев, в каждом из которых необхо- димо выполнить несколько шагов операций с узлами.
>
> **Вопрос**. Почему для обхода двоичного дерева в глубину существуют три по- рядка -- прямой, симметричный и обратный, и в чем их преимущества?
>
> **Ответ**. Подобно прямому и обратному обходу массива, прямой, симметрич- ный и обратный обходы являются тремя методами обхода двоичного дерева. Они позволяют получить результат обхода в определенном порядке. Напри- мер, в двоичном дереве поиска, поскольку значения узлов удовлетворяют ус- ловию «значение левого дочернего узла \< значение корневого узла \< значение правого дочернего узла», обход дерева в порядке «левый → корень → правый» позволяет получить упорядоченную последовательность узлов.
>
> **Вопрос**. Операция правого вращения обрабатывает отношения между не- сбалансированным узлом node, дочерним узлом child и внуком grand_child. Не нужно ли поддерживать связь node с его родительским узлом?
>
> **Ответ**. Необходимо рассматривать этот вопрос с рекурсивной точки зрения. Операция правого вращения right_rotate(root) принимает корневой узел под- дерева, и в итоге возвращает child, который после вращения становится кор- невым узлом. Связь корневого узла поддерева с его родительским узлом уста- навливается после завершения функции и не входит в область поддержания операции правого вращения.
>
> **Вопрос**. В C++ функции разделяются на private и public. Какие соображения при этом нужно учитывать? Почему функции height() и updateHeight() разме- щены в public и private соответственно?
>
> **Ответ**. Это зависит от области применения метода. Если метод использу- ется только внутри класса, его следует сделать private. Например, вызов up- dateHeight() пользователем не имеет смысла, так как это лишь этап в опера- циях вставки и удаления. А height() используется для доступа к высоте узла аналогично методу vector.size(), поэтому он помечен как public для удобства использования.
>
> **Вопрос**. Как построить двоичное дерево поиска из набора входных данных?
>
> Является ли важным способ выбора корневого узла?
>
> **Ответ**. Да, метод построения дерева описан в методе build_tree() в коде дво- ичного дерева поиска. Что касается выбора корневого узла, обычно входные
>
> данные сортируются, затем средний элемент выбирается в качестве корневого узла, после чего рекурсивно строятся левые и правые поддеревья. Это позво- ляет максимально сохранить баланс дерева.
>
> **Вопрос**. Всегда ли в Java для сравнения строк нужно использовать метод
>
> equals()?
>
> **Ответ**. В Java для базовых типов данных оператор == используется для срав- нения значений двух переменных. Для ссылочных типов принцип работы этих операторов различен.
>
> ① ==: используется для сравнения того, указывают ли две переменные на один и тот же объект, т. е. совпадают ли их позиции в памяти.
- equals(): используется для сравнения значений двух объектов.
> Таким образом, если необходимо сравнить значения, следует использовать метод equals(). Однако строки, инициализированные как String a = \"hi\"; String b = \"hi\";, хранятся в пуле строковых констант и указывают на один и тот же объект, поэтому для сравнения содержимого этих строк можно использовать a == b.
>
> **Вопрос**. До достижения самого нижнего уровня при обходе в ширину коли- чество узлов в очереди равно 2*h*?
>
> **Ответ**. Да, например, для полного двоичного дерева высотой *h* = 2 общее количество узлов *n* = 7, тогда количество узлов на нижнем уровне равно 4 = 2*h* = (*n* + 1)/2.
>
> Глава 8