使用 O(1) 辅助存储空间删除二叉树中的所有节点?

Posted

技术标签:

【中文标题】使用 O(1) 辅助存储空间删除二叉树中的所有节点?【英文标题】:Deleting all nodes in a binary tree using O(1) auxiliary storage space? 【发布时间】:2012-12-11 04:08:15 【问题描述】:

删除binary tree 中所有节点的标准算法在这些节点上使用postorder traversal

if (root is not null) 
   recursively delete left subtree
   recursively delete right subtree
   delete root

该算法使用 O(h) 辅助存储空间,其中 h 是树的高度,因为在递归调用期间需要存储堆栈帧的空间。但是,它运行时间为 O(n),因为每个节点都只被访问过一次。

是否有一种算法可以在不牺牲运行时间的情况下仅

【问题讨论】:

【参考方案1】:

确实可以使用基于tree rotations的算法,使用O(n)和O(1)辅助存储空间删除二叉树中的所有节点。

给定一棵具有以下形状的二叉树:

           u
          / \
         v   C
        / \
       A   B

这棵树的右旋转将节点 v 拉到节点 u 上方,并产生以下树:

        v
       / \
      A   u
         / \
        B   C

注意,树的旋转可以在 O(1) 的时间和空间内完成,只需将树的根更改为 v,将 u 的左孩子设置为 v 的前右孩子,然后将 v 的右孩子设置为 u .

树旋转在这种情况下很有用,因为右旋转总是会将树的左子树的高度减一。这很有用,因为一个聪明的观察:如果树的根没有左子,删除它是非常容易的。特别是,如果树的形状是这样的:

     v
      \
       A

那么我们可以通过删除节点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's right child the new root.
    

这个算法显然只使用了 O(1) 的存储空间,因为它最多需要恒定数量的指针来进行旋转或更改根,并且这些指针的空间可以在循环的所有迭代中重复使用。

此外,可以证明该算法也可以在 O(n) 时间内运行。直观地说,可以通过查看给定边缘可以旋转多少次来看到这一点。首先,请注意,每当执行右旋转时,从节点到其左子节点的边将转换为从前一个子节点返回其父节点的右边。接下来,请注意,一旦我们执行将节点 u 移动到节点 v 的右子节点的旋转,我们将永远不会再接触节点 u,直到我们删除节点 v 和 v 的所有左子树。因此,我们可以通过注意树中的每条边最多与其父节点一起旋转一次来限制将要完成的总旋转次数。因此,最多完成 O(n) 次旋转,每次旋转需要 O(1) 时间,并且恰好完成 n 次删除。这意味着该算法在 O(n) 时间内运行并且仅使用 O(1) 空间。

如果有帮助,我有 a C++ implementation of this algorithm,以及对算法行为的更深入分析。它还包括算法所有步骤正确性的正式证明。

希望这会有所帮助!

【讨论】:

【参考方案2】:

让我从一个严肃的笑话开始:如果您将 BST 的根设置为空,您实际上会删除树中的所有节点(垃圾收集器将使空间可用)。虽然措辞是特定于 Java 的,但这个想法也适用于其他编程语言。我提到这一点是为了以防你参加工作面试或参加考试。

否则,您只需使用DSW algorithm 的修改版本即可。基本上将树变成主干,然后像linked list 一样删除。空间 O(1) 和时间 O(n)。你应该在任何教科书或网上找到关于 DSW 的讨论。

基本上 DSW 用于平衡 BST。但是对于您的情况,一旦获得主干,而不是平衡,您就像删除链表一样删除。

【讨论】:

有趣的是,DSW 算法使用与我上面提出的几乎相同的算法将树变成主干:向右旋转直到没有左孩子,然后在右孩子上重复。所以从某种意义上说,我的答案是 DSW 第一步与删除步骤相结合的优化版本。感谢您推荐 DSW 方法! @templatetypedef 我刚刚阅读了您的帖子。好工作!看起来我用更少的词来给出和你一样的答复。投票给你!【参考方案3】:

算法1O(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;
        
    

算法2O(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】:

我对上面所有需要复杂操作的答案感到惊讶。

通过简单地将所有递归调用替换为搜索节点并跟踪当前节点的父节点的循环,可以使用 O(1) 额外存储从 BST 中删除节点。使用递归更简单,因为递归调用会自动将搜索节点的所有祖先存储在堆栈中。但是,不必存储所有祖先。只需要存储搜索到的节点和它的父节点,这样搜索到的节点就可以被取消链接。存储所有祖先简直是浪费空间。

Python 3 中的解决方案如下。不要被对delete 的看似递归调用所迷惑——这里的最大递归深度是 2,因为第二次调用 delete 保证会导致删除基本情况(包含搜索值的根节点)。

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

【讨论】:

以上是关于使用 O(1) 辅助存储空间删除二叉树中的所有节点?的主要内容,如果未能解决你的问题,请参考以下文章

使用 O(1) 辅助空间迭代二叉树

打印二叉树中所有节点的祖先。我们可以在小于 O(n^2) 的时间复杂度内完成它吗?

LeetCode 二叉树专项删除二叉搜索树中的节点(450)

计算具有给定高度的二叉树中的所有节点

堆(插入删除)

⭐算法入门⭐《二叉树 - 二叉搜索树》中等05 —— LeetCode 450. 删除二叉搜索树中的节点