从二叉搜索树中删除后子节点分配的原因?

Reason of child node assignments after deletion from Binary Search Tree?

在学习BST删除机制的过程中,有一点没有理解。你能解释一下为什么每次调用 Delete(Node* p, int key) 时都有一个赋值 (p->rchild =, p->lchild =) 吗?实际上,我认为 Delete(Node* p, int key) 方法只是保持返回而没有任何变化,所以树不会改变。

当我在寻找解释时,我偶然发现了这句话:

We have to make assignments after deletion else we will end up having duplicate nodes.

如果你同意这个说法,能否请你解释一下?

Node* BST::Delete(Node *p, int key) {
    Node* q;
 
    if (p == nullptr){
        return nullptr;
    }
 
    if (p->lchild == nullptr && p->rchild == nullptr){
        if (p == root){
            root = nullptr;
        }
        delete p;
        return nullptr;
    }
 
    if (key < p->data){
        p->lchild = Delete(p->lchild, key);
    } else if (key > p->data){
        p->rchild = Delete(p->rchild, key);
    } else {
        if (Height(p->lchild) > Height(p->rchild)){
            q = InPre(p->lchild);
            p->data = q->data;
            p->lchild = Delete(p->lchild, q->data);
        } else {
            q = InSucc(p->rchild);
            p->data = q->data;
            p->rchild = Delete(p->rchild, q->data);
        }
    }
    return p;
}

why there is an assignment (p->rchild =, p->lchild =) each time the Delete(Node* p, int key) is called?

如果找到数据,那么目标就是拥有一棵少了一个节点的树。该算法将使用 value-swapping 机制来确保实际将被删除的节点始终是叶节点。一个叶子节点的删除包括两个动作:

  1. 从内存中删除;
  2. 更新其 parent 以便将引用此已删除节点的 child 指针设置为空指针。

由于算法向右递归到该叶节点,因此无法将空指针设置为其 parent,因为在此阶段没有对 parent 节点可用的引用。为此 caller 应该做一些事情,因为调用者 do 有对 parent.

的引用

所以当递归遍历到叶子节点时,需要传达给调用者这个节点应该被detach。它通过 returning 一个空指针来实现,协议是调用者(其当前节点 p 是 parent)应该将 returned 指针分配给相关的child 指针。这样,删除的节点就真正与树的其余部分分离了。

Actually, I thought that the Delete(Node* p, int key) method just keeps returning without any mutation so the tree doesn't change.

树肯定必须以某种方式更改才能从中删除节点。更改发生在 p->lchildp->rchild

的赋值中

I stumbled into this sentence :

We have to make assignments after deletion else we will end up having duplicate nodes.

这是事实。让我们举一个例子树:

                 7
                / \
               3   8
              / \   \
             1   5   9
            /   / \ 
           0   4   6

现在让我们看看调用 Delete(root, 3) 会发生什么。 p指向值为7的节点,我们通过递归调用向左走:

p->lchild = Delete(p->lchild, key);

在递归执行上下文中,我们得到一个新的p,它指向值为3的节点。这就是我们要查找的值,所以我们进入外部else堵塞。由于该节点下方子树的高度相等,我们进入内部 else 块。我们在那里分配:

q = InSucc(p->rchild);

q 将引用值为 4 的节点。现在发生了重复。我们将数据从 q 复制到 p。这归结为从树中删除值 3:

p->data = q->data;

但是现在树中的值 4 是原来的两倍。

                 7
                / \
               4*  8
              / \   \
             1   5   9
            /   / \ 
           0   4*  6

所以算法下降到(右)child,现在寻求删除该子树中值为 4 的节点:

p->rchild = Delete(p->rchild, q->data);

在这个新的递归调用中,我们再次得到一个新的 p,它现在引用值为 5 的节点。我们向左移动——这个赋值稍后将发挥重要作用:

p->lchild = Delete(p->lchild, key);

这个最终的递归调用有一个新的 p,它引用值为 4 的节点——我们正在寻找的节点。

这一次我们在具有 deleteif 块中结束,因为该节点是叶节点。节点被释放,一个空指针被 returned 指向调用者。从这里我们开始回溯树。

所以向上一层,在值为 5 的节点,我们从递归调用(它是一个空指针)中得到 return 值并赋值给它:

p->lchild = Delete(p->lchild, key);

这项重要的任务将从树中分离重复节点(值为 4)。您可以看到,如果不进行此分配,仍然会引用具有重复值的该节点——即使它指向已释放的内存。

这棵树现在处于最终形状:

                 7
                / \
               4   8
              / \   \
             1   5   9
            /     \ 
           0       6

回溯仍将继续,回到根。也对一些 child 指针进行了赋值,但这些不会改变树,因为在所有这些情况下我们 returned return p;,这是调用者的原始值 child指针。

错误

如评论中所述,代码有一个错误。删除叶节点时,不会验证该节点是否确实具有要删除的值。因此,如果您使用树中不存在的值调用此方法,您将最终删除具有另一个值的叶节点。在上面的示例树中:如果您调用 Delete(root, 10),将删除值为 9 的节点。

要更正此错误,请移动以下 if 块:

if (p->lchild == nullptr && p->rchild == nullptr){

... 在外部 else 块内,作为那里的第一个语句。