图解红黑树

红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。在了解红黑树之前我们需要简述一下二叉查找树。

BST

二叉查找树,也称有序二叉树,是指一棵空树或者具有以下性质的二叉树:

  • 左子节点的值比父节点小
  • 右子节点的值比父节点大
  • 任意节点的左右字树也分别为二叉查找树
  • 没有键值相等的点

《图解红黑树》

BST

在理想情况下,二叉查找树增删改查的时间复杂度为O(logN),但是若是二叉树极度不平衡,比如下图这样形成了一个线性链后,就会产生最坏运行情况O(N).

《图解红黑树》

最坏运行复杂度

基于BST存在的问题,平衡二叉树产生了,典型的有AVL树和红黑树,因为AVL是严格的平衡二叉树,但是插入和删除的性能较差,所以在实际生产环境中不如红黑树应用广泛。

RBTree

红黑树的应用非常广泛,常见的函数库,如C++中的map,multimap,以及Java中的TreeMap,TreeSet, Java8中的HashMap的实现也采用了红黑树。

红黑树从本质上来说就是一颗二叉查找树,但是在二叉树的基础上增加了着色相关的性质,使得红黑树可以保证相对平衡,从而保证红黑树的增删改查的时间复杂度最坏也能达到O(log N)。

下面是红黑树最重要的5条性质,后面需要正常回来查看:

  1. 每个节点要么是黑的,要么是红的
  2. 根节点是黑的
  3. 叶节点是黑的
  4. 如果一个节点是红的,他的两个儿子节点都是黑的
  5. 对于任一节点而言,其到叶节点树尾端NIL指针的每一条路径都包含相同数目的黑节点

《图解红黑树》

红黑树(图片引自维基百科)

上图是一棵典型的红黑树,红黑树的5条特性确保了从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,使得整棵树大致上是平衡的。树上的增删改查操作的最坏情况时间都与树的高度成正比,所以红黑树在最坏情况下也是高效的。

在红黑树中一般用黑的NIL节点表示叶节点,不包含值,只是标志该分支结束,有时候绘图中会直接省略。

在正式介绍红黑树插入和删除操作前,需要先了解红黑树的旋转操作。

RBTree的旋转操作

当在含n个关键字的红黑树上进行insert和delete操作时,修改后的树可能不满足上面给出的5个红黑树的基本特性,所以需要改变树中的某些节点的颜色以及指针结构。 这些指针结构的修改是通过旋转完成的,旋转分为左旋和右旋:

《图解红黑树》

旋转操作

当在某个节点x上做左旋时,我们假设它的右孩子是y节点并且不为NIL。左旋以x到y之间的轴为支撑,左旋后,y成为该局部新的根,x成为y的做孩子,而y的左孩子成为x的右孩子,即图中的β。我们以一段伪代码说明左旋的过程:

y<-right[x]   //把x的右儿子保存为y
right[x]<-left[y]   //把y的左儿子给x作为右儿子
p[left[y]]<-x   //把x设为y的左儿子的爸爸,这一步与上一步对应,因为指针都是双向的
p[y]<-p[x]      //把x的爸爸设定为y的爸爸
if p[x]=nil[T]    //如果x的爸爸本来就是空的
  then root[T]<-y  //那么y就变成了根节点
  else if x=left[p[x]]   //否则,如果原来x是它爸爸的左儿子
    then left[p[x]]<-y   //就把y设为原来x爸爸的左儿子
    else right[p[x]]<-y   //不然就把y设为原来x爸爸的右儿子
left[y]<-x     //x这时候转下来成为y的左儿子
p[x]<-y    //y 也就成了x的爸爸了

在实际的树中,旋转如下所示:

《图解红黑树》

旋转前

旋转后,18代替了11成为了7的右儿子,11,成了18的左儿子,18原先的儿子,即以14为根节点的左子树成了11的右儿子,如下所示:

《图解红黑树》

旋转后

右旋操作与左旋类似,读者们可以自行试着写出伪代码。

RBTree的插入操作

红黑树的插入与BST的插入方式是一样的,也是通过不断比较大小,插入到合适位置,只不过插入后需要做调整,以满足红黑树的5条特性。

先看一下BST的插入代码:

 public BinaryNode<T> insert(T t,BinaryNode<T> node)
   {
       if(node==null)
       {
           return new BinaryNode<T>(t, null, null);
       }
       int result = t.compareTo(node.data);
       if(result<0)
          node.left= insert(t,node.left);
       else if(result>0)
          node.right= insert(t,node.right);
       else
           ;//doNothing
       return node;
   }

新插入的节点总是设为红色的,所以如果父节点为黑色,就不需要修复,因为没有任何性质被改变,所以只有在父节点为红色节点时需要做修复操作。

修复操作一共分为3种情况:

情况1 :当前节点的父节点是红色,且祖父节点的另一个子节点(叔叔节点)也是红色

此时,我们只考虑当前节点是父节点的左儿子,因为当前节点和父节点都是红色,不满足红黑树的特性4 (如果一个节点是红的,他的两个儿子节点都是黑的)。

对策 :把父节点和叔叔节点变黑,爷爷节点涂红,然后把当前节点指针给到爷爷,让爷爷节点那层继续循环,接受红黑树特性检测。

《图解红黑树》

1.PNG

新插入节点1,父亲2和叔叔6都是红的,所以把2和6都变成黑的, 爷爷变成红的。

情况2: 当前节点的父节点是红的,叔叔节点是黑的,当前节点是父节点的右子树。

对策:当前节点的父节点作为新的当前节点,以新当前指点为支点左旋

接着上面的情况1, 在情况1的操作之后,当前节点为4, 但是4的父节点3还是红的,仍然不满足红黑树的特性4, 由于叔叔节点7是黑的,且4为3的右儿子,所以满足情况2,这时候把当前节点转移到3,以3为支点左旋。4变为爷爷5的新左儿子,而原来的3下去了,成为4的左儿子。4原有的左儿子(2-1分支)给3做了右儿子。

《图解红黑树》

2.PNG

情况3:当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左儿子

对策: 父节点变黑,祖父变红,以祖父节点为支点右旋

接着上面的情况2, 当前节点变成了3,由于父节点4为红,叔叔节点7 为黑,且4为5的左儿子,所以满足情况3,所以需要将父节点4变黑,祖父5变红,并且右旋,右旋后,4 的右儿子6跟了5做了左儿子,5成了4的右儿子。如图所示:

《图解红黑树》

3.PNG

这样红黑树重新恢复了平衡。上面的例子里正好遇到了三种基本情况,实际操作中也许一步就成功了,简化的来看,也就是下面三种基本情况:
case1 :

《图解红黑树》

父红叔红

case2 :

《图解红黑树》

父红, 左子

case3 :

《图解红黑树》

父红, 右子

当前节点为父节点的右节点的时候,先旋转成为左子,即case2的样子,再右旋。

修复操作整体来说是一个向上回溯的过程,就拿我们举得例子来说,情况1操作后,当前节点向上移到了4, 此时又会检测4 和它的父节点之间性质是否符合红黑树的5条特性,不对的话继续修复红黑树,直到当前节点转移到root节点,且root节点为黑色,修复操作结束。

RBTree的删除操作

删除操作首先也需要做普通BST的删除操作,删除操作会删除对应的节点,叶子节点就会直接删除,如果是非叶子节点,就会用中序遍历的后继节点来顶替要删除的节点,有的书上也会用前驱节点来顶替。删除后也需要做修复操作,来满足红黑树的特性。

首先也是看一下BST删除节点的代码:

public BinaryNode<T> remove(T t,BinaryNode<T> node)
{
       if(node == null)
           return node;//没有找到,doNothing
       int result = t.compareTo(node.data);
       if(result>0)
           node.right = remove(t,node.right);
       else if(result<0)
           node.left = remove(t,node.left);
       else if(node.left!=null&&node.right!=null) 
       {   //待删除节点有两个儿子节点时,用右儿子中的最小元素填充待删除节点
           node.data = findMin(node.right).data;
           node.right = remove(node.data,node.right);
       }
       else //只有一个儿子的时候,把父节点的儿子指针指向儿子的该独生子
           node = (node.left!=null)?node.left:node.right;
       return node;        
}

删除修复操作在遇到被删除的节点是红色节点或者到达root节点时,修复操作完毕。

在删除一个节点后,如果删除的节点时红色节点,那么红黑树的性质并不会被影响,此时不需要修正,如果删除的是黑色节点,原红黑树的性质就会被改变,此时我们需要做修正。

当黑色节点被删除后,假设该节点为y,会产生3个问题:

  1. 如果y原来是根节点,而y的一个红色孩子成为新的根,则违反了性质2
  2. 如果y的子节点和y的父节点都是红色,那么y被删除后,两个连续的红色节点连接起来,违反了性质4
  3. 删除y将导致先前包含y的任何路径上的黑节点个数少1,性质5被破坏

现在我们假设,顶替删除节点的那个后来节点,继承了被删除的黑色节点的那层黑色,也就是说顶替的节点具有双重颜色,如果原来是黑色,那么现在就是黑+黑,如果原来是红色,现在就是红+黑。因为有了这层额外的黑色,所以性质5还是能保持的,现在只需要恢复它的性质即可。

  • 如果当前节点时红+黑色
    直接把当前节点染成黑色,此时红黑树性质全部恢复
  • 如果当前节点时黑+黑且是根节点,此时什么都不用做,直接结束
  • 如果当前节点时黑+黑,但是不是根节点,那么又可以分为以下4种情况

设删除节点为x节点

情况1:x节点时黑+黑,且x的兄弟节点是红色(x的父节点和兄弟节点的子节点都是黑色)

《图解红黑树》

x节点的兄弟节点为红色(x节点的父节点和兄弟节点的子节点都是黑色)

因为兄弟节点7必须有黑色孩子,我们可以改变4和7的颜色,再对4做一次左旋,而且红黑性质继续保存。完成这两个操作后,尽管所有路径上黑色节点的数目没有改变,但现在删除节点有了一个黑色的兄弟和一个红色的父亲(它的新兄弟是黑色因为它是原先红7的一个儿子),所以我们可以接下去按情形2、情形3或情形4来处理。

情况2:x节点时黑+黑,x的兄弟节点时黑色(x的兄弟节点的两个孩子都是黑色)

《图解红黑树》

x的兄弟节点时黑色(x的兄弟节点的两个孩子都是黑色)

此时我们把x的兄弟节点转变为红色,设置x的父节点为新的当前节点。
其实这样做的思想是把原先x中的一个黑色属性上移。原先的x变成单纯的黑节点,而它的父节点4此时变成了红+黑,如果4原来就是黑,那么此时变成黑+黑。此时左边分支的黑节点数没有变化,但是右边4,7那一条分支,黑节点数增加了1,因为此时4也包含黑属性,所以需要通过减1个黑色节点,因为兄弟节点7的子节点都是黑的,所以直接把7 变成红的。
经过上面的步骤,黑色属性转移到4中去了,这时候继续对4进行处理。

情况3:x节点是黑+黑节点,x的兄弟节点时黑色(x的兄弟节点的左儿子是红,右儿子是黑)

《图解红黑树》

x的兄弟节点时黑色(x的兄弟节点的左儿子是红,右儿子是黑)

此时我们把x的兄弟节点的左儿子设为黑色,将兄弟节点设为红色,再对兄弟节点进行右旋。并且重新设置旋转后x的兄弟节点,此时是5. 其实这一步只是一个中间状态,并且不是平衡的,目的是为了得到情况4的状态

情况4:x节点是黑+黑节点,x的兄弟节点时黑色(x的兄弟节点的右儿子是红,左儿子随意)

《图解红黑树》

x的兄弟节点时黑色(x的兄弟节点的右儿子是红,左儿子随意)

此时,把x的父节点的颜色赋给x的兄弟节点,把父节点设为黑色,将x的兄弟节点的右子节点设为黑色,再对x的父节点进行左旋。这一步操作的真正的节点借调操作,通过将兄弟节点以及兄弟节点的右节点借调过来,并将兄弟节点的右子节点变成红色来达到借调两个黑节点的目的,这样的话,整棵树还是符合红黑树的定义的

    原文作者:算法小白
    原文地址: https://juejin.im/entry/5a0e38f06fb9a0451e3f5bed
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞