二叉查找树的实现中, 删除算法是最难的, 对于我来说, 尤其困惑了好长时间, 需要画图及一些想象力. 由于我的类中有一些成员对方法提供了支持, 所以先看看类的声明:
public class BinaryTree {
public TreeNode root; // 树根
private TreeNode parent; // 表示父节点
private boolean isLeft; // 用来判断节点在父节点的哪一边
.....
}
当然删除的一切前提是这个节点在树中要存在, 所以查找是必不可少的, 在 find 方法中, 我将父节点保存了下来, 以简化操作:
public TreeNode find (TreeNode key) {
TreeNode current = root;
while ( current.data != key.data ) {
parent = current; // 保存父节点的引用
if ( key.data < current.data ) { // 当前节点小就往左
isLeft = true;
current = current.left;
}
else { // 否则就往右
isLeft = false;
current = current.right;
}
if ( current == null ) { // 没有找到
return null;
}
}
return current;
}
那么删除的开始应该是这样的:
public boolean delete (TreeNode nodeToDel) {
TreeNode current = find (nodeToDel); // 先找一下
if ( current == null ) { // 没找到就什么都不用做了
return false;
}
...............
如果找到了, 下面有三种情况:
一. 要删除的节点没有子节点, 即它是叶节点
这是最简单的一种情况, 只要找到它, 然后根据它是在父节点的左边还是右边, 将父节点相应位置的引用设为空即可, 当然它也有可能是根, 要判断一下:
if ( current.left == null && current.right == null ) { // 如果没有子节点
if ( current == root ) { // 是根节点, 那这颗树就没有了
root = null;
}
else if ( isLeft ) { // 如果它在父节点的左边
parent.left = null; // 将父节点左孩子引用设为空
}
else { // 否则就是在右边
parent.right = null;
}
}
二. 要删除的节点有一个子节点
这个情况也比较简单, 就是让这个要删除节点的孩子代替它的位置即可, 如果它是父节点的左孩子, 就让它的子节点成为父节点的左孩子, 以此类推:
..............
else if ( current.right == null ) { // 这个节点只有左孩子
if ( current == root ) { // 如果它是根, 那它的左孩子就成为新的根了
root = current.left;
}
else if ( isLeft ) { // 要删除的节点是它父节点的左孩子
parent.left = current.left; // 让它的子节点成为父节点的左孩子 (代替它)
}
else { // 不在左边就在右边
parent.right = current.left;
}
}
else if ( current.left == null ) { // 这个节点只有右孩子
if ( current == root ) {
root = current.right;
}
else if ( isLeft ) {
parent.left = current.right;
}
else {
parent.right = current.right;
}
}
三. 要删除的节点有两个子节点
最复杂的就在这里了, 因为我已经不能用它的子节点来代替它了, 这将多出一节点, 应该怎样安排它呢? 这里的窍门是用它的中序后继来代替它, 只有这样才能保持二叉查找树的特征, 所以第一步是找中序后继, 就是比要删除节点大的节点中最小的那个节点. 不过我觉得最先看看要执行哪些操作会比较清楚一些. 这分两情况:
1. 要删除节点的后继就是它的右子节点, 就是说它的右子节点没有左孩子
这样删除操作有两步:
a) 用后继来代替要删除的节点的位置
b) 把要删除节点的左孩子变成后继的左孩子 (记得后继是没有左孩子的)
else { // 接上面代码, 要删除节点有两个子节点
TreeNode successor = getSuccessor (nodeToDel); // 得到中序后继
if ( current == root ) { // 执行步骤 a
root = successor;
}
else if ( isLeft ) {
parent.left = successor;
}
else {
parent.right = successor;
}
successor.left = current.left; // 执行步骤 b ,
}
} // end of delete
2. 要删除节点的后继是它的右子节点的左孩子, 就是说它的右子节点有左孩子
这样删除操作有四步, 这个四步包含前面两步, 但我还是都列出来:
a) 用后继来代替要删除的节点的位置
b) 把要删除节点的左孩子变成它后继的左孩子
c) 把后继的右孩子变成后继父节点的左孩子 ( 记得后继是没有左孩子的)
d) 把要删除节点的右孩子变成它后继的右孩子
想一下这些步骤, 看起来很乱, 首先我们知道要删除的节点有两个孩子, 而且它的后继有一个右孩子, 那么删除以后, 我们要考虑的事情有:
谁来代替它? (步骤a) ,
如何安排它的左孩子? (步骤b).
如何安排它的右孩子? (步骤d).
如何安排它后继的右孩子? (步骤c).
一个节点了中序后继无论如何是没有左孩子的, 这是由二叉查找树的特征决定的, 它是 这个节点右子树中的最小值, 如果它有左孩子, 那这个左孩子必定比它小, 它也就不可成为中序后继. 来看看如何获得中序后继的代码:
private TreeNode getSuccessor (TreeNode nodeToDel ) {
TreeNode successorParent, successor; // 后继和后继的父节点
successorParent = successor = nodeToDel;
TreeNode current = nodeToDel.right; // 从右边开始找, 因为要比它大
while ( current != null ) {
successorParent = successor;
successor = current;
current = current.left; // 一直往左找, 直到左孩子为空
}
// 实际上已经找到后继, 但是由于已经有了后继和后继父节点的引用,
// 所以在这里进行步骤 c 和 步骤 d 是最适合的.
// 只有后继不是要删除节点的右孩子的时候, 才执行这两步, 这样和前面代码
// 一起执行了四步, 否则, 只执行两步.
if ( successor != nodeToDel.right ) {
successorParent.left = successor.right; // 步骤 c
successor.right = nodeToDel.right; // 步骤 d
}
return successor;
}
结尾:
TreeNode 只是对一个整数和两个引用的简单封装, 没有把代码放在这里. 希望这个简单的分析能够有所帮助, 不过有点怀疑有没有人会象我这样被这个问题难倒. :).