这个 BST 节点删除算法是如何工作的?
Posted
技术标签:
【中文标题】这个 BST 节点删除算法是如何工作的?【英文标题】:How does this BST node-deletion algorithm work? 【发布时间】:2012-07-07 23:27:11 【问题描述】:我正在尝试遵循"Data Structures and Algorithms" by Granville Barnett 中的 BST 算法,但我不明白它在下面描述的节点删除算法。
第 3.3 节(第 22 页)
从 BST 中删除节点相当简单,需要考虑四种情况:
要删除的值是叶节点;或 要删除的值有右子树,但没有左子树;或 要删除的值有一个左子树,但没有右子树;或 要移除的值同时具有左子树和右子树,在这种情况下,我们提升左子树中的最大值。
图 3.2(第 22 页)
23
/ \
14 31
/
7
\
9
案例 #1 指向节点 9。
案例 #2 指向节点 7。
案例 #3 指向节点 14。
案例 #4 指向节点 23。
我将上面 #4 的文本解释为,当我们删除 23 时,我们将 14 提升为 root 并使 31 成为其右孩子:
14
/ \
7 31
\
9
...但是案例 #4 的书中算法(从第 23 页开始)让我感到困惑(我在这里用 Java 重写了它):
1 boolean remove(T value)
2 // ...
3
4 // case #4
5 Node largestValueNode = nodeToRemove.left;
6 while (largestValueNode.right != null)
7 // find the largest value in the left subtree of nodeToRemove
8 largestValueNode = largestValueNode.right;
9
10
11 // delete the right child of largestValueNode's parent
12 findParent(largestValueNode.value).right = null;
13 nodeToRemove.value = largestValueNode.value;
14
15 count--;
16 return true; // successful
17
如果我按照算法,largestValueNode
是节点14,所以它的父节点是节点23。为什么算法会取消父节点的右孩子?
为什么第 13 行将largestValueNode
的值复制到要删除的节点中?
我希望第 11-13 行是:
11 if (largestValueNode != null)
12 largestValueNode.right = nodeToRemove.right;
13 nodeToRemove.right = null;
编辑:
这本书的算法确实有bug。修复如下:
1 boolean remove(T value)
2 // ...
3
4 // case #4
5 Node largestValueNode = nodeToRemove.left;
6 while (largestValueNode.right != null)
7 // find the largest value in the left subtree of nodeToRemove
8 largestValueNode = largestValueNode.right;
9
10
11 Node p = findParent(largestValueNode.value);
12 if (p != null)
13 if (nodeToRemove == p)
14 nodeToRemove.left = largestValueNode.left;
15 else
16 p.right = largestValueNode.left;
17
18 nodeToRemove.value = largestValueNode.value;
19
20 count--;
21 return true; // successful
22
【问题讨论】:
注意 - 您从您引用的文本中删除了关键行。就在count--
之前,文本有以下行:nodeToRemove.Value = largestValueNode.Value
,您在这里没有看到。
它没有丢失。您可能正在查看问题的结尾,这是我自己的代码,我期望算法是。书中的实际代码就在我自己之前。
你说得对——我在看问题的结尾。谢谢指正。
【参考方案1】:
如果你这样做
11 if (largestValueNode != null)
12 largestValueNode.right = nodeToRemove.right;
13 nodeToRemove.right = null;
你没有考虑14
可能有一个正确的孩子的情况。例如:
23
/ \
14 31
/ \
7 15
\
9
删除23
时的解决方案应该是
15
/ \
14 31
/
7
\
9
因此,您将 15
的原始父级 14
的右孩子设置为 null。这就是第一个代码正在做的事情。
编辑:处理您的评论
有了你的解决方案,你会得到
23
/
14
/ \
7 15
\ \
9 31
另外,原来的代码也是错误的;试试这样的:
if(nodeToRemove == findParent(largestValueNode.value))
nodeToRemove.left = largestValueNode.left
else
findParent(largestValueNode.value).right = largestValueNode.left
nodeToRemove.value = largestValueNode.value
还要回答,“为什么第13行将最大值节点的值复制到要删除的节点中?”
我们正在删除largestValueNode
,在此之前我们将其值存储在nodeToRemove
中
【讨论】:
第 5-9 行搜索要删除的节点的左子树中的最大节点。 没有任何右孩子来标识最大节点。因此,如果树中有 15 个,largestValueNode
将是 15(没有正确的孩子)。
@user46874 编辑了答案以澄清我试图提出的观点。另外,注意到原来的代码也是错误的。我已经提供了我的解决方案。
谢谢,现在说得通了。用您的代码替换第 11-13 行可以解决问题。
很高兴知道原始代码是错误的——我刚刚花了几个小时在上面,一直在想我错过了什么。还有一个NPE问题,也没有办法删除根元素。
很高兴知道原始代码是错误的——我刚刚花了几个小时在上面,一直在想我错过了什么。如果通过了根元素,则存在 NPE 问题,并且无论如何都不会考虑根元素的删除。【参考方案2】:
对于这个特定的例子,这本书的算法似乎是错误的(假设你已经完美地翻译成 Java :))。它正在做你提到的事情,但它适合这种情况:
其中 nodeToRemove = 23 并且在您的 BST 14 中有一个右孩子 15。本书的算法将在这里用 15 替换 23 并将 14 的右孩子设置为空。在这种情况下,您的算法将失败。
【讨论】:
请注意,该问题在所引用的文本中留下了一行代码,从而解决了此问题。请参阅问题下方的评论。 ... 事实上,这行代码并没有丢失(见问题下方的 cmets)。【参考方案3】:仔细看线:
largestValueNode.right = nodeToRemove.right;
注意这一行如何使14
看起来像这样(忽略孙子):
14
/ \
7 31
但这正是我们想要的!因为14
现在有31
作为它的右孩子,所以31
不再是15
的右孩子,所以为了清理,15
的右孩子被设置为 NULL .
【讨论】:
您引用的行是我自己的代码。我期待这会出现在本书建议的算法中。而且没有孩子15
...
你说得对——我显然是草率的。我同意原始代码不正确。【参考方案4】:
很高兴知道原始代码是错误的 - 我刚刚在上面花了几个小时,一直在想我错过了什么。如果通过了根元素,则存在 NPE 问题,并且无论如何都没有考虑根元素的删除。
这是我的 Java 实现,可能会使用一些优化 - 欢迎提出建议。 O (n log n)
最坏的情况。下面进行测试。
public boolean remove(final T value0)
BinarySearchTreeNode<T> target = findNode(value0);
// Node DNE
if (target == null)
return false;
// Both children populated, no need for parent
if (target.right != null && target.left != null)
BinarySearchTreeNode<T> max = maxChild(target.left);
findParent(max.value).right = null;
target.value = max.value;
// Root element targeted, parent DNE
else if (target == root)
if (target.right == null && target.left == null)
root = null;
else if (target.right == null)
root = target.left;
else
root = target.right;
// Non-root, single-child node - find if L or R child, update parent reference.
else
BinarySearchTreeNode<T> parent = findParent(value0);
if (target.right == null && target.left != null)
if (target.value.compareTo(parent.value) < 0)
parent.left = target.left;
else
parent.right = target.left;
else if (target.right != null && target.left == null)
if (target.value.compareTo(parent.value) < 0)
parent.left = target.right;
else
parent.right = target.right;
return true;
单元测试(显然都通过了):
package BinarySearchTreeTests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
public class Remove
BinarySearchTree<Integer> tree;
@Before
public void setUp()
tree = new BinarySearchTree<Integer>();
@Test
public void fromEmptyTree()
assertFalse(tree.remove(8));
@Test
public void fromTreeWithOnlyRootNode()
tree.add(10);
assertTrue(tree.remove(10));
assertNull(tree.root);
@Test
public void nonexistentElement()
tree.add(10);
assertFalse(tree.remove(8));
/**
* N
* 10--|
* | 6
* 5--|
* 3
*/
@Test
public void nodeWithNoRightChildren()
tree.add(10);
tree.add(5);
tree.add(6);
tree.add(3);
tree.remove(10);
assertEquals(tree.root.value, Integer.valueOf(5));
assertEquals(tree.root.left.value, Integer.valueOf(3));
assertEquals(tree.root.right.value, Integer.valueOf(6));
/**
* 17
* 15--|
* | 13
* 10--|
* N
*/
@Test
public void nodeWithNoLeftChildren()
tree.add(10);
tree.add(15);
tree.add(17);
tree.add(13);
tree.remove(10);
assertEquals(tree.root.value, Integer.valueOf(15));
assertEquals(tree.root.left.value, Integer.valueOf(13));
assertEquals(tree.root.right.value, Integer.valueOf(17));
/**
* 19
* 17-|
* | 16
* 15-|
* | | 14
* | 13-|
* | 12
* 10--|
* N
*/
@Test
public void nodeWithLeftAndRightChildren()
tree.add(10);
tree.add(15);
tree.add(17);
tree.add(13);
tree.add(19);
tree.add(16);
tree.add(14);
tree.add(12);
tree.remove(15);
assertEquals(tree.root.right.value, Integer.valueOf(14));
assertNull(tree.root.right.left.right);
/**
* 18
* 15-|
* | [ALWAYS EMPTY]
* 15-|
* | | 13
* | 12-|
* | 11
* 10--|
* N
*
@Test
public void removeDuplicate()
Above diagram shows duplicate cases are already tested implicitly.
fail();
*/
【讨论】:
以上是关于这个 BST 节点删除算法是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章