Loading

红黑树详细讲解(结合JavaTreeMap)

 

1:红黑树简介

红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。根据二叉查找树的概念可以得出正常情况下查找的时间复杂度为O(log n),但是可能会出现一种极端的情况使得这颗二叉树变为线性的则查找的时间复杂度直接降到(O(n)),为了避免这种情况红黑树应运而生,红黑树是一颗自平衡的二叉树,是解决二叉查找树变为线性结构的一种实现方式。

1.1:红黑树概念:

1、每个节点都只能是红色或者黑色

2、根节点是黑色

3、每个叶节点(NIL节点,空节点)是黑色的。

4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。

5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。

 

1.2:红黑树自平衡的关键操作

1:左旋转(如下图:图片来源网络)

 

 

 

2:右旋转:(如下图:图片来源网络)

 

 

 

3:变色:通过改变节点的颜色达到满足红黑树的概念。

 

1.3:红黑树的应用

红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。 例如,Java集合中的TreeSet,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。

2:红黑树的插入操作

2.1插入

插入总共可以分为五种情况,其中前两种情况是最简单的,而后面的情况又是可以相互转换的,下面具体分析。为了使插入之后尽量满足红黑树更多的规则,所以我们规定每个新插入的节点为红色。

(第三四五种情况我们按照在如下前提下进行:插入的节点为S(son),S的父亲节点为F(father),F的父亲节点为G(grandfather),而F的兄弟节点为U(uncle)。并且F为G的左儿子) 当F为G的右儿子的时候对称操作即可。

2.1.1:为跟节点

若新插入的节点N没有父节点,则直接当做根据节点插入即可,同时将颜色设置为黑色。

2.1.2:父节点为黑色

这种情况新节点N同样是直接插入,同时颜色为红色,由于根据规则四它会存在两个黑色的叶子节点,值为null。同时由于新增节点N为红色,所以通过它的子节点的路径依然会保存着相同的黑色节点数,同样满足规则5。

2.1.3:若父节点F和F的兄弟节点U都为红色

操作:将F以及U设置为黑,G设为红,并将G看成新插入的节点(即下一轮的S),递归操作。

 

 

 

 

原因:这个操作实际是想将红色往根处移动。将红色往上移了一层,并不会打破红黑树的特性,不断的把红色往上移动,当移动到根时,直接将根设置为黑色,就完全符合红黑树的性质了。

2.1.4:若父节点F为红色,叔节点U为黑色或者缺少,且新增节点S为F节点的右孩子

操作:将F左旋,并把F看成新加入的节点(即下一轮的S),继续后面的判断。

 

 

 

这里所产生的结果其实并没有完成,还不是平衡的(违反了规则四),这是我们需要进行情况5的操作。

2.1.5:父节点F为红色,叔父节点U为黑色或者缺少,新增节点S为父节点F左孩子

操作:先将F设为黑,G设为红,然后G右旋。这样操作后,就完全符合红黑树的性质了

 

 

 

3:Java中TreeMap插入操作实现分析

本来想自己手动实现一下红黑树,奈何JDK源码写的太优秀了,下面就结合Java中TreeMap的put方法实现分析上面的操作。

3.1:插入操作

public V put(K key, V value) {
   Entry<K,V> t = root;
   //第一种情况也就是上满的2.1.1
   if (t == null) {
       //本来感觉这句代码没有用,看到源码注释才知道校验空值用的,真是的。。。。。。。
       compare(key, key); // type (and possibly null) check

       root = new Entry<>(key, value, null);
       size = 1;
       modCount++;
       return null;
  }
   //cmp表示key比较大小的返回结果  
   int cmp;
   Entry<K,V> parent;
   // split comparator and comparable paths
   Comparator<? super K> cpr = comparator;
   //如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合  
   if (cpr != null) {
       //该循环是根据给定的排序算法根据key是否能找到对应的键值对,如果能找到的话则覆盖旧值并且返回旧值
       do {
           //parent为找到的符合插入的父节点
           parent = t;
           cmp = cpr.compare(key, t.key);
           if (cmp < 0)
               t = t.left;
           else if (cmp > 0)
               t = t.right;
           else
               return t.setValue(value);
      } while (t != null);
  }
   //如果cpr为空,则采用默认的排序算法进行创建TreeMap集合  
   else {
       if (key == null)
           throw new NullPointerException();
       @SuppressWarnings("unchecked")
           Comparable<? super K> k = (Comparable<? super K>) key;
       //该循环是根据默认的排序算法根据key是否能找到对应的键值对,如果能找到的话则覆盖旧值并且返回旧值
       do {
           //parent为找到的符合插入的父节点
           parent = t;
           cmp = k.compareTo(t.key);
           if (cmp < 0)
               t = t.left;
           else if (cmp > 0)
               t = t.right;
           else
               return t.setValue(value);
      } while (t != null);
  }
   //如果当前key不再此树种,则新建节点,插入的parent节点下
   Entry<K,V> e = new Entry<>(key, value, parent);
   if (cmp < 0)
       //当前key小于父节点key,插入左子树
       parent.left = e;
   else
       //当前key大于父节点key,插入右子树
       parent.right = e;
   //插入节点之后调整,使其满足红黑树的性质,其中情况2.1.2是不需要调整的,所以调整操作里面没有情况2.1.2
   fixAfterInsertion(e);
   size++;
   modCount++;
   return null;
}

。3.2:调整操作

调整红黑树节点,使其满足红黑树的五条性质。

private void fixAfterInsertion(Entry<K,V> x) {
//使插入的节点为红色
  x.color = RED;
//循环 直到 x不是根节点,且x的父节点不为红色
  while (x != null && x != root && x.parent.color == RED) {
  //如果插入的节点的父节点是他的父节点的左节点,也就是我们在2.1上面假设的F为G的左儿子
      if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
      //获取x的叔叔节点
          Entry<K,V> y = rightOf(parentOf(parentOf(x)));
              //如果X的叔节点(U) 为红色(情况2.1.3)
          if (colorOf(y) == RED) {
          //将X的父节点设置为黑色  
              setColor(parentOf(x), BLACK);
              //将X的叔叔节点设置为黑色  
              setColor(y, BLACK);
              //将X的祖父节点设置为红色  
              setColor(parentOf(parentOf(x)), RED);
              //把祖父节点当为当前节点循环递归
              x = parentOf(parentOf(x));
          }
            //如果X的叔节点,这里会存在两种情况(2.1.4和2.1.5)
          else {
          //如果X节点为其父节点的右子树,则进行左旋转(情况2.1.4)
              if (x == rightOf(parentOf(x))) {
                  x = parentOf(x);
                  //左旋转代码不再展示,不明白怎么实现左旋转代码的可以自己参考一下源码
                  rotateLeft(x);
              }
              //情况(2.1.5)
              //将X的父节点设置为黑色  
              setColor(parentOf(x), BLACK);
                //将X的祖父节点设置为黑色  
              setColor(parentOf(parentOf(x)), RED);
              //右旋转
              rotateRight(parentOf(parentOf(x)));
          }
      }
        //如果插入的节点的父节点是他的父节点的右节点,也就是我们在2.1上面假设的F为G的右儿子
        下面的操作均为上面操作的对称操作,不再多余解释
      else {
          Entry<K,V> y = leftOf(parentOf(parentOf(x)));
          if (colorOf(y) == RED) {
              setColor(parentOf(x), BLACK);
              setColor(y, BLACK);
              setColor(parentOf(parentOf(x)), RED);
              x = parentOf(parentOf(x));
          } else {
              if (x == leftOf(parentOf(x))) {
                  x = parentOf(x);
                  rotateRight(x);
              }
              setColor(parentOf(x), BLACK);
              setColor(parentOf(parentOf(x)), RED);
              rotateLeft(parentOf(parentOf(x)));
          }
      }
  }
  //由于上面的递归操作可能使root节点变为红色,不满足红黑树性质,所有设置root节点为黑色
  root.color = BLACK;
}

4:红黑树的删除操作

相对于红黑树的增加节点而言,删除更加复杂,因为删除节点的时候相对于插入多了很多情况,实际上只要我们慢慢的捋清楚所有的情况,你就会发现他也不难,然后自己完全搞明白之后说不会还会感叹,就这!就这!哈哈哈!

由于红黑树的叶节点都是nil节点,所以为了表述方便,我们下面nil节点不再考虑。

4.1:删除

红黑树的删除主要分为一下几种情况,我们下面一一讨论,然后再结合Java中TreeMap中的删除操作分析一下。

4.1.1:情况1

待删除节点为红色,且没有子节点,这这种情况是最简单的,因为删除子节点之后不会影响红黑树的限制条件,所以直接删除即可。

4.1.2:情况2

待删除有一个子节点,由于红黑树限制,X1只能为红色,不理解的可以在重温一下概念。

这种情况处理也是非常简单的,用子节点替代待删除节点,然后删除子节点即可。

 

 

 

4.1.3:情况3

待删除节点有两个子节点,这种情况说明待删除节点不是叶节点,那么我们就需要找到他的后继节点,然后用后继节点的key和value覆盖待删除节点,然后再去删除后继节点,这样就将待删除节点转换到了叶节点上或者叶节点的父节点上,有些情况就转换成情况1和情况2,剩下的情况就是待删除节点为黑色,且没有子节点这一种情况。然后这一种情况又可以分为四种情况,哈哈哈,是不是有点惊喜,有点意外。

顺便说一下前驱和后继的概念

前驱:小于当前节点的最大节点
后继:大于当前节点的最小节点

4.1.4:情况4

待删除节点的兄弟节点为红色

B为红色,那么其子节点BL、BR必定全部为黑色,父节点P也为黑色。处理策略是:改变B、P的颜色,然后进行一次左旋转。这样处理就可以使得红黑性质得以继续保持。这样处理后将情况情况4、转变为情况5、6、7中的一种。

 

 

 

 

 

 

4.1.5:情况5

s的兄弟B是黑色的,且B的俩个孩子都是黑色的

 

 

 

这种情况其父节点P可红可黑(图上画的为黑色,不要误解),由于B为黑色,这样导致S子树相对于其兄弟B子树少一个黑色节点,这时我们可以将B置为红色。这样,S子树与B子树黑色节点一致,保持了平衡。

将B由黑转变为红,这样就会导致新节点S(也就是图上的节点P)相对于它的兄弟节点会少一个黑色节点。但是如果P为红色,我们直接将P转变为黑色,保持整棵树的平衡。否则情况情况5 会转变为情况4、6、7的一种。

 

4.1.6:情况6

S的兄弟B是黑色的,B的左孩子是红色,B的右孩子是黑色。

 

 

 

这种情况是将节点B和其左子节点进行颜色交换,然后对B进行右旋转处理。于是情况6转化为情况7

4.1.7:情况7

S的兄弟B是黑色的,且B的右孩子时红色的

 

 

 

 

交换B和父节点P的颜色,同时对P进行左旋转操作。这样就把左边缺失的黑色节点给补回来了。同时将B的右子节点BR置黑。这样左右都达到了平衡。

后面的这四种情况比较难理解,首先他们都不是单一的某种情况,他们之间是可以进行互转的。相对于其他的几种情况,情况5比较好理解,仅仅只是一个颜色的转变,通过减少右子树的一个黑色节点使之保持平衡,同时将不平衡点上移至S与B的父节点,然后进行下一轮迭代。情况4,是将B旋转将其转成情况5、6、7情况进行处理。而情况6通过转变后可以化成情况7来进行处理,从这里可以看出情况7应该最终结。情况7、右子节点为红色节点,那么将缺失的黑色节点交由给右子节点,通过旋转达到平衡。

5:Java中TreeMap的删除操作实现分析

5.1:删除操作

public V remove(Object key) {
//根据查找节点
  Entry<K,V> p = getEntry(key);
  //查找不到,直接返回
  if (p == null)
      return null;
//查找到,删除节点P并返回P节点的value
  V oldValue = p.value;
  deleteEntry(p);
  return oldValue;
}

 

private void deleteEntry(Entry<K,V> p) {
  modCount++;
  size--;

  // If strictly internal, copy successor's element to p and then make p
  // point to successor.
  //情况3,左右节点都不为空,查找后继,用后继的key和value覆盖待删除的节点
  if (p.left != null && p.right != null) {
  //寻找后继节点
      Entry<K,V> s = successor(p);
      p.key = s.key;
      p.value = s.value;
      p = s;
  } // p has 2 children

  // Start fixup at replacement node, if it exists.
  //replacement为替代节点,如果P的左子树存在那么就用左子树替代,否则用右子树替代
  Entry<K,V> replacement = (p.left != null ? p.left : p.right);

//如果replacement不为空,则说明有子节点,则满足情况二,直接删除此节点,并用子节点替代皆即可
  if (replacement != null) {
      // Link replacement to parent
       
      replacement.parent = p.parent;
      //删除节点为root节点
      if (p.parent == null)
          root = replacement;
      else if (p == p.parent.left)
          p.parent.left = replacement;
      else
          p.parent.right = replacement;

      // Null out links so they are OK to use by fixAfterDeletion.
      p.left = p.right = p.parent = null;

      // Fix replacement
      //情况2:如果待删除节点P为黑色,则调整P节点的子节点为黑色
      if (p.color == BLACK)
          fixAfterDeletion(replacement);
  } else if (p.parent == null) { // return if we are the only node.
  //p没有父节点,表示为P根节点,直接删除即可
      root = null;
  } else { // No children. Use self as phantom replacement and unlink.
  //如果P节点的颜色为黑色,则需要按照情况4、5、6、7对红黑树进行调整  
      if (p.color == BLACK)
          fixAfterDeletion(p);

//P节点不存在子节点,直接删除即可,
      if (p.parent != null) {
          if (p == p.parent.left)
              p.parent.left = null;
          else if (p == p.parent.right)
              p.parent.right = null;
          p.parent = null;
      }
  }
}

5.2:调整操作

private void fixAfterDeletion(Entry<K,V> x) {
  while (x != root && colorOf(x) == BLACK) {
  //如果X的父亲节点是其祖父节点的左孩子
      if (x == leftOf(parentOf(x))) {
      //获取兄弟节点
          Entry<K,V> sib = rightOf(parentOf(x));
//情况4:兄弟节点为红色,改变B、P的颜色,然后进行一次左旋转
          if (colorOf(sib) == RED) {
              setColor(sib, BLACK);
              setColor(parentOf(x), RED);
              rotateLeft(parentOf(x));
              sib = rightOf(parentOf(x));
          }
//情况5:若兄弟节点的两个子节点都为黑色,将兄弟节点变成红色
          if (colorOf(leftOf(sib)) == BLACK &&
              colorOf(rightOf(sib)) == BLACK) {
              setColor(sib, RED);
              x = parentOf(x);
          } else {
          //情况6:如果兄弟节点只有右子树为黑色,将兄弟节点与其左子树进行颜色互换然后进行右转
              if (colorOf(rightOf(sib)) == BLACK) {
                  setColor(leftOf(sib), BLACK);
                  setColor(sib, RED);
                  rotateRight(sib);
                  sib = rightOf(parentOf(x));
              }
              //情况7:交换兄弟节点和父节点的颜色,同时将兄弟节点右子树设置为黑色,最后左旋转
              setColor(sib, colorOf(parentOf(x)));
              setColor(parentOf(x), BLACK);
              setColor(rightOf(sib), BLACK);
              rotateLeft(parentOf(x));
              x = root;
          }
      }
      //如果X的父亲节点是其祖父节点的右孩子,与上面的操作对称即可,不再做多余解释
      else { // symmetric
          Entry<K,V> sib = leftOf(parentOf(x));

          if (colorOf(sib) == RED) {
              setColor(sib, BLACK);
              setColor(parentOf(x), RED);
              rotateRight(parentOf(x));
              sib = leftOf(parentOf(x));
          }

          if (colorOf(rightOf(sib)) == BLACK &&
              colorOf(leftOf(sib)) == BLACK) {
              setColor(sib, RED);
              x = parentOf(x);
          } else {
              if (colorOf(leftOf(sib)) == BLACK) {
                  setColor(rightOf(sib), BLACK);
                  setColor(sib, RED);
                  rotateLeft(sib);
                  sib = leftOf(parentOf(x));
              }
              setColor(sib, colorOf(parentOf(x)));
              setColor(parentOf(x), BLACK);
              setColor(leftOf(sib), BLACK);
              rotateRight(parentOf(x));
              x = root;
          }
      }
  }
//设置节点为黑色
  setColor(x, BLACK);
}

这是红黑树在删除节点后,对树的平衡性进行调整的过程,其实现过程与上面四种复杂的情况一一对应,所以在这个源码的时候一定要对着上面提到的四种情况看。

小结:本文主要结合Java中TreeMap的实现详细的说明红黑树的插入和删除操作以及调整过程,红黑树的主要操作是插入和删除,插入和删除的时候难免会破坏红黑树的结构,所有我们需要变色,左旋转,右旋转来调整,使其满足红黑树的限制条件。

 

posted @ 2021-08-02 00:20  Philosophy  阅读(275)  评论(0编辑  收藏  举报