Удаление всех узлов в двоичном дереве с использованием O (1) дополнительного пространства для хранения?
Стандартный алгоритм для удаления всех узлов в двоичном дереве использует по узлам в этих строках:
if (root is not null) {
recursively delete left subtree
recursively delete right subtree
delete root
}
В этом алгоритме используется вспомогательное пространство хранения O (h), где h - высота дерева, из-за пространства, необходимого для хранения кадров стека во время рекурсивных вызовов. Однако он работает во времени O (n), потому что каждый node посещается ровно один раз.
Есть ли алгоритм для удаления всех узлов в двоичном дереве, используя только O (1) вспомогательное пространство для хранения, не жертвуя временем выполнения?
Ответы
Ответ 1
Действительно, можно удалить все узлы в двоичном дереве с использованием вспомогательного пространства хранения O (n) и O (1) с помощью алгоритма, основанного на вращения дерева.
Учитывая двоичное дерево со следующей формой:
u
/ \
v C
/ \
A B
Правое вращение этого дерева вытягивает node v над node u и приводит к следующему дереву:
v
/ \
A u
/ \
B C
Обратите внимание, что вращение дерева может быть выполнено в O (1) времени и пространстве, просто изменив корень дерева на v, установив u левый дочерний элемент в прежний правый дочерний элемент, а затем установив v правильный дочерний элемент u.
Вращение деревьев полезно в этом контексте, потому что правильное вращение всегда будет уменьшать высоту левого поддерева дерева на единицу. Это полезно из-за умного наблюдения: очень просто удалить корень дерева, если у него нет левой подщелицы. В частности, если дерево имеет такую форму:
v
\
A
Затем мы можем удалить все узлы в дереве, удалив node v, а затем удалив все узлы в своем поддереве A. Это приводит к очень простому алгоритму для удаления всех узлов в дереве:
while (root is not null) {
if (root has a left child) {
perform a right rotation
} else {
delete the root, and make the root right child the new root.
}
}
В этом алгоритме явно используется только пространство памяти O (1), потому что ему требуется не более постоянного количества указателей для вращения или для изменения корня, и пространство для этих указателей может быть повторно использовано на всех итерациях цикла.
Кроме того, можно показать, что этот алгоритм также работает в O (n) времени. Интуитивно, это можно увидеть, посмотрев, сколько раз может быть повернуто данное ребро. Во-первых, обратите внимание, что всякий раз, когда выполняется правильное вращение, ребро, которое идет от node к его левому дочернему элементу, преобразуется в правый край, который исходит от прежнего ребенка назад к его родительскому. Затем обратите внимание, что как только мы выполним поворот, который перемещает node u в качестве правильного дочернего элемента node v, мы никогда не коснемся node u снова, пока не удалим node v и все v поддеревья слева. В результате мы можем связать количество полных поворотов, которые когда-либо будут выполнены, отметив, что каждое ребро в дереве будет вращаться с его родителем не более одного раза. Следовательно, выполняется не более O (n) поворотов, каждый из которых принимает время O (1), и сделано ровно n удалений. Это означает, что алгоритм работает во времени O (n) и использует только O (1) пространство.
В случае, если это помогает, у меня есть реализация этого алгоритма на С++, а также гораздо больше, глубинный анализ поведения алгоритма. Он также включает формальные доказательства правильности для всех этапов алгоритма.
Надеюсь, это поможет!
Ответ 2
Позвольте мне начать с серьезной шутки: если вы установите корень BST равным null, вы фактически удалите все узлы в дереве (сборщик мусора сделает доступным пространство). Хотя формулировка является специфичной для Java, идея сохраняется для других языков программирования. Я упоминаю об этом на всякий случай, когда вы были на собеседовании или сдавали экзамен.
В противном случае все, что вам нужно сделать, это использовать измененную версию DSW algorithm
. В основном превратите дерево в основу и затем удалите, как вы бы linked list
. Пространство O (1) и время O (n). Вы должны найти переговоры о DSW в любом учебнике или в Интернете.
В основном DSW используется для баланса BST. Но для вашего случая, как только вы получите основу, вместо балансировки, вы удаляете, как и связанный список.
Ответ 3
Алгоритм 1, O (n) время и O (1) пространство: немедленно удалить узел, если у него нет обоих дочерних элементов. В противном случае доберитесь до самого левого узла, переворачивая "левые" ссылки, чтобы убедиться, что все узлы достижимы - самый левый узел становится новым корнем:
void delete_tree(Node *node) {
Node *p, *left, *right;
for (p = node; p; ) {
left = p->left;
right = p->right;
if (left && right) {
Node *prev_p = nullptr;
do {
p->left = prev_p;
prev_p = p;
p = left;
} while ((left = p->left) != nullptr);
p->left = p->right;
p->right = prev_p; //need it on the right to avoid loop
} else {
delete p;
p = (left) ? left : right;
}
}
}
Алгоритм 2, время O (n) и пространство O (1): пересекают узлы в глубину, заменяя дочерние ссылки ссылками на родительские. Каждый узел удаляется по пути вверх:
void delete_tree(Node *node) {
Node *p, *left, *right;
Node *upper = nullptr;
for (p = node; p; ) {
left = p->left;
right = p->right;
if (left && left != upper) {
p->left = upper;
upper = p;
p = left;
} else if (right && right != upper) {
p->right = upper;
upper = p;
p = right;
} else {
delete p;
p = upper;
if (p)
upper = (p->left) ? p->left : p->right;
}
}
}
Ответ 4
Я удивлен всеми ответами выше, которые требуют сложных операций.
Удаление узлов из BST с O (1) дополнительной памятью возможно, просто заменив все рекурсивные вызовы циклом, который ищет узел и также отслеживает родительский узел текущего узла. Использовать рекурсию проще, потому что рекурсивные вызовы автоматически сохраняют всех предков искомого узла в стеке. Однако не обязательно хранить всех предков. Необходимо только сохранить искомый узел и его родительский элемент, чтобы искомый узел можно было отсоединить. Хранение всех предков - просто пустая трата пространства.
Решение в Python 3 ниже. Не сбрасывайте со стороны, казалось бы, рекурсивный вызов на delete
- максимальная глубина рекурсии здесь равна 2, поскольку второй вызов на удаление гарантированно приведет к базовому случаю удаления (корневой узел, содержащий искомое значение).
class Tree(object):
def __init__(self, x):
self.value = x
self.left = None
self.right = None
def remove_rightmost(parent, parent_left, child):
while child.right is not None:
parent = child
parent_left = False
child = child.right
if parent_left:
parent.left = child.left
else:
parent.right = child.left
return child.value
def delete(t, q):
if t is None:
return None
if t.value == q:
if t.left is None:
return t.right
else:
rightmost_value = remove_rightmost(t, True, t.left)
t.value = rightmost_value
return t
rv = t
while t is not None and t.value != q:
parent = t
if q < t.value:
t = t.left
parent_left = True
else:
t = t.right
parent_left = False
if t is None:
return rv
if parent_left:
parent.left = delete(t, q)
else:
parent.right = delete(t, q)
return rv
def deleteFromBST(t, queries):
for q in queries:
t = delete(t, q)
return t