二叉查找树的删除算法

二叉查找树的实现中, 删除算法是最难的, 对于我来说, 尤其困惑了好长时间, 需要画图及一些想象力. 由于我的类中有一些成员对方法提供了支持, 所以先看看类的声明:

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 只是对一个整数和两个引用的简单封装, 没有把代码放在这里. 希望这个简单的分析能够有所帮助, 不过有点怀疑有没有人会象我这样被这个问题难倒. :).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值