10 KiB
Двоичное дерево поиска
Как показано на рисунке ниже, двоичное дерево поиска (binary search tree) удовлетворяет следующим условиям.
- Для корневого узла значения всех узлов в левом поддереве
<значения корневого узла<значения всех узлов в правом поддереве. - Левое и правое поддеревья любого узла также являются двоичными деревьями поиска, т. е. также удовлетворяют условию
1..
Операции с двоичным деревом поиска
Мы инкапсулируем двоичное дерево поиска в класс BinarySearchTree и объявляем переменную-член root, указывающую на корневой узел дерева.
Поиск узла
Для заданного значения целевого узла num можно выполнить поиск, используя свойства двоичного дерева поиска. Как показано на рисунке ниже, мы объявляем узел cur, начиная с корневого узла root двоичного дерева, и циклически сравниваем значение узла cur.val с num.
- Если
cur.val < num, это означает, что целевой узел находится в правом поддеревеcur, поэтому выполняемcur = cur.right. - Если
cur.val > num, это означает, что целевой узел находится в левом поддеревеcur, поэтому выполняемcur = cur.left. - Если
cur.val = num, это означает, что целевой узел найден, выходим из цикла и возвращаем этот узел.
Операция поиска в двоичном дереве поиска работает по тому же принципу, что и алгоритм бинарного поиска: в каждом раунде исключается половина случаев. Количество циклов не превышает высоты двоичного дерева, когда двоичное дерево сбалансировано, используется O(\log n) времени. Пример кода приведен ниже:
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
Вставка узла
Для заданного элемента num, который необходимо вставить, чтобы сохранить свойство двоичного дерева поиска "левое поддерево < корневой узел < правое поддерево", процесс вставки выглядит следующим образом.
- Поиск позиции для вставки: аналогично операции поиска, начиная с корневого узла, циклически выполняем поиск вниз в зависимости от соотношения между значением текущего узла и
num, пока не выйдем за пределы листового узла (достигнемNone), после чего выходим из цикла. - Вставка узла в эту позицию: инициализируем узел
numи помещаем этот узел на местоNone.
В реализации кода необходимо обратить внимание на следующие два момента.
- Двоичное дерево поиска не допускает существования дублирующихся узлов, иначе это нарушит его определение. Поэтому, если узел, который необходимо вставить, уже существует в дереве, вставка не выполняется и происходит прямой возврат.
- Для реализации вставки узла нам необходимо использовать узел
preдля сохранения узла из предыдущего раунда цикла. Таким образом, когда мы достигаемNone, мы можем получить его родительский узел и завершить операцию вставки узла.
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
Как и при поиске узла, вставка узла использует O(\log n) времени.
Удаление узла
Сначала находим целевой узел в двоичном дереве, затем удаляем его. Аналогично вставке узла, нам необходимо обеспечить, чтобы после завершения операции удаления свойство двоичного дерева поиска "левое поддерево < корневой узел < правое поддерево" по-прежнему выполнялось. Поэтому, в зависимости от количества дочерних узлов целевого узла, мы различаем 3 случая: 0, 1 и 2, и выполняем соответствующую операцию удаления узла.
Как показано на рисунке ниже, когда степень удаляемого узла равна 0, это означает, что узел является листовым и может быть удален напрямую.
Как показано на рисунке ниже, когда степень удаляемого узла равна 1, достаточно заменить удаляемый узел его дочерним узлом.
Когда степень удаляемого узла равна 2, мы не можем удалить его напрямую, а должны использовать другой узел для замены этого узла. Чтобы сохранить свойство двоичного дерева поиска "левое поддерево < корневой узел < правое поддерево", этим узлом может быть минимальный узел правого поддерева или максимальный узел левого поддерева.
Предположим, мы выбираем минимальный узел правого поддерева (следующий узел в симметричном обходе), тогда процесс удаления выглядит следующим образом.
- Находим узел, следующий за удаляемым узлом в "последовательности симметричного обхода", обозначим его как
tmp. - Заменяем значение удаляемого узла значением
tmpи рекурсивно удаляем узелtmpв дереве.
Операция удаления узла также использует O(\log n) времени, где поиск удаляемого узла требует O(\log n) времени, получение узла-преемника в симметричном обходе требует O(\log n) времени. Пример кода приведен ниже:
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
Упорядоченность симметричного обхода
Как показано на рисунке ниже, симметричный обход двоичного дерева следует порядку обхода "левый \rightarrow корневой \rightarrow правый", а двоичное дерево поиска удовлетворяет соотношению размеров "левый дочерний узел < корневой узел < правый дочерний узел".
Это означает, что при выполнении симметричного обхода двоичного дерева поиска всегда сначала обходится следующий наименьший узел, что приводит к важному свойству: последовательность симметричного обхода двоичного дерева поиска является возрастающей.
Используя свойство возрастания симметричного обхода, мы можем получить упорядоченные данные в двоичном дереве поиска всего за O(n) времени, без необходимости выполнения дополнительных операций сортировки, что очень эффективно.












