红黑树(Red-Black Tree)解析

这一篇我们来聊聊红黑树,写这篇文章的起因是在阅读HashMap源码时,发现JDK1.8对于HashMap的实现引入了红黑树来处理哈希冲突以提高性能(戳这里,有详述),而红黑树的数据结构和操作都是较为复杂的,自己看得过程中有些地方也反复了多次。。。俗话说得好,好记性不如烂笔头,因此决定写下这篇笔记供自己和需要的人日后参考。在开始之前,首先要感谢张拭心同学的这两篇关于红黑树和二叉查找树的文章:

http://blog.csdn.net/u011240877/article/details/53242179

http://blog.csdn.net/u011240877/article/details/53329023

这两篇文章讲得十分详细,使我受益匪浅,在这里也强烈推荐大家阅读一下。由于拭心同学的文章在分析二叉查找树的查找,插入和删除时引用的是递归的实现,为了不重复,本文分析时将采用循环的实现,为大家提供另一种思路。

OK,正式开始,何为红黑树?红黑树(Red-Black Tree) 是一种自平衡二叉查找树,其每个节点都带有黑或红的颜色属性。由于它的本质也是一种二叉查找树,因此它的查找,插入和删除操作均以二叉查找树的对应操作作为基础;但由于红黑树自身要保证平衡(也即要始终满足其五条特性,这个下文会有详述),每次插入和删除之后它都要进行额外的调整,以恢复自身的平衡,这是它与普通二叉查找树不同的地方,也正因为如此,红黑树的查找,插入和删除操作在最坏情况下的时间复杂度也能保证为O(logN),其中N为树中元素个数。

既然红黑树本质是二叉查找树,那么就有必要先来看一下二叉查找树的相关知识。

  • 二叉查找树

    二叉查找树(Binary Search Tree),又名二叉排序树,二叉搜索树,B树。顾名思义,它的节点是可比较的并且具有以下性质:

    a. 若左子树不为空,则根节点的值大于其所有左子树中节点的值;
    b. 若右子树不为空,则根节点的值小于或等于其所有右子树中节点的值;
    c. 左右子树也分别为二叉查找树;
    d. 没有键值相等的节点。

    由于以上性质,中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉查找树变成一个有序序列,构造树的过程即为对无序序列进行查找的过程。每次插入的新的结点都是二叉查找树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。搜索、插入、删除的复杂度等于树高,期望 O(logN),最坏 O(N)(数列有序,树退化成线性表)。

    这里先给出一个二叉查找树节点的结构,下文代码中就用它作为树节点的类:

    class BSTNode{
        int value;  //节点的值
        BSTNode left;  //节点的左子树
        BSTNode right;  //节点的右子树
        BSTNode parent;  //节点的父节点
    
        BSTNode(int value, BSTNode parent) {
            this.value = value;
            this.parent = parent;
        }
    
        @Override
        public boolean equals(Object obj)
        {
              //两个节点的value相等,则认为两个节点相等
            return (obj instanceof BSTNode) && (((BSTNode) obj).value == this.value);
        }
    }

    下面就分别看一下二叉查找树的查找,插入和删除操作的实现,此处采用循环来实现。

    • 查找

      在二叉搜索树T中查找key的过程为:

      a. 若T是空树,则搜索失败,否则:
      b. 若key等于T的根节点的数据域之值,则查找成功;否则:
      c. 若key小于T的根节点的数据域之值,则搜索左子树;否则:
      d. 查找右子树。

      下面是这个过程的Java代码实现:

      /**
      * @param key 目标节点的键值
      * @return 与key匹配的节点,若未能成功匹配则返回null
      */
      BSTNode searchBST(int key) {
          //若根节点为空,或根节点与key匹配成功,则直接返回
          if (mRoot == null || mRoot.value == key) {
              return mRoot;
          }
      
          BSTNode t = mRoot;
      
          //从根节点开始循环查找
          do {
              if (key < t.value) {
                  t = t.left; //若key比节点小,则在左子树中继续查找
              }
              else if (key > t.value) {
                  t = t.right; //若key比节点大,则在右子树中继续查找
              }
              else {
                  return t; //匹配成功,返回匹配节点
              }
          }
          while (t != null);
      
          return null; //匹配失败,返回null
      }  
    • 插入

      插入可以理解为先查找,找到了就说明已经存在该节点不用再进行插入了(也有可能找到后做覆盖操作,比如HashMap的put方法),找不到就将指针最后停留的叶子节点当做待插入节点的父节点,根据两个节点值的大小关系确定该作为左子树还是右子树插入。下面是相关代码:

      /**
      * @param key 待插入节点的键值
      */
      void insertBST(int key) {
          if (mRoot == null) {
              //若根节点为空,则使用key创建根节点,插入完成
              mRoot = new BSTNode(key, null);
              return;
          }
      
          BSTNode t = mRoot;
          BSTNode parent; //指向当前遍历到的节点的指针
      
          //从根节点开始循环查找
          do {
              parent = t;
      
              if (key < t.value) {
                  t = t.left; //若key比节点小,则在左子树中继续查找
              }
              else if (key > t.value) {
                  t = t.right; //若key比节点大,则在右子树中继续查找
              }
              else {
                  return; //若key与节点的值相等,则说明节点已存在,不需要插入,直接返回(若需要覆盖节点,在这里完成)
              }
          }
          while (t != null);
      
          //执行到这一步说明值为key的节点不存在,新创建一个节点,将parent指针指向的节点作为父节点
          BSTNode nodeToInsert = new BSTNode(key, parent);
      
          if (key < parent.value) {
              parent.left = nodeToInsert; //若key比parent的值小,则作为parent的左子树插入
          }
          else {
              parent.right = nodeToInsert; //若key比parent的值大,则作为parent的右子树插入
          }
      }
    • 删除

      删除操作第一步也是查找,找到待删除节点后分下列几种情况:

      a. 若节点为子节点,直接删除即可;

      b. 若节点只有左子树或右子树,则删除该节点后,将其唯一的子树与父节点相连;

      c. 若节点有两个子树,则需要选择一个子树,并从中选出合适的节点K与待删除节点的父节点相连。这时树的结构会发生变化,节点K将接替待删除节点作为这一棵子树的根,那么显然,K需要大于其左子树的所有节点且小于右子树的所有节点。这里对于K有两种选择,要么选择待删除节点的左子树中最大的节点,要么选择其右子树中最小的节点,二者皆可,我们选择前者来实现。下面是删除操作相关代码:

      /**
      * @param key 待删除节点的键值
      */
      void deleteBST(int key) {
          if (mRoot == null) {
              return; //若树为空,则无法删除,返回
          }
      
          BSTNode t = mRoot;
          BSTNode nodeToDelete = null; //需要删除的节点
      
          //循环查找待删除节点
          do {
              if (key < t.value) {
                  t = t.left; //在左子树中继续
              }
              else if (key > t.value) {
                  t = t.right; //在右子树中继续
              }
              else {
                  nodeToDelete = t; //匹配成功,找到待删除节点,退出循环
                 
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值