红黑树R-B-tree

http://blog.csdn.net/v_july_v/article/details/6105630
http://www.cnblogs.com/skywang12345/p/3245399.html
https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.01.md
http://daoluan.net/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%AE%97%E6%B3%95/2013/09/25/rbtree-is-not-difficult.html
http://www.cnblogs.com/hzmark/archive/2013/01/02/TreeMap-Base.html
http://liangjiabin.com/blog/2015/04/red-black-tree.html
https://www.ibm.com/developerworks/cn/java/j-lo-tree/

二叉查找树

由于红黑树本质上就是一棵二叉查找树,所以在了解红黑树之前,咱们先来看下二叉查找树。

二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若任意结点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
任意结点的左、右子树也分别为二叉查找树。
没有键值相等的结点(no duplicate nodes)。
因为,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的执行时间为O(lgn).(至于n个结点的二叉树高度为lgn的证明,可参考算法导论 第12章 二叉查找树 第12.4节)。

但二叉树若退化成了一棵具有n个结点的线性链后,则此些操作最坏情况运行时间为O(n)。后面我们会看到一种基于二叉查找树-红黑树,它通过一些性质使得树相对平衡,使得最终查找、插入、删除的时间复杂度最坏情况下依然为O(lgn)。

红黑树

前面我们已经说过,红黑树,本质上来说就是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

但它是如何保证一棵n个结点的红黑树的高度始终保持在h = logn的呢?这就引出了红黑树的5条性质:

1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。

正是红黑树的这5条性质,使得一棵n个结点是红黑树始终保持了logn的高度,从而也就解释了上面我们所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论的原因。

红黑树R-B-tree_第1张图片
上文中我们所说的 “叶结点” 或”NULL结点”,它不包含数据而只充当树在此结束的指示,这些结点以及它们的父结点,在绘图中都会经常被省略。

左旋 和 右旋

红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。(无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树)
旋转包括两种:左旋 和 右旋。

左旋

红黑树R-B-tree_第2张图片

对x进行左旋,意味着”将x变成一个左节点”。
当在某个结点x上,做左旋操作时,我们假设它的右孩子y不是NIL[T],x可以为任何不是NIL[T]的节点的左孩子结点。
左旋以x到y之间的链为“支轴”进行,它使y成为该孩子树新的根,而y的左孩子β则成为x的右孩子。

LEFT-ROTATE(T, x)  {
     y ← x.right            // 前提:这里假设x的右孩子为y。下面开始正式操作
     x.right ← y.left      // 将 “y的左孩子” 设为 “x的右孩子”,即 将β设为x的右孩子
     y.left.parent ← x          // 将 “x” 设为 “y的左孩子的父亲”,即 将β的父亲设为x
     y.parent ← x.parent             // 将 “x的父亲” 设为 “y的父亲”
     if x.parent = T.nil       
         then T.root ← y                 // 情况1:如果 “x的父亲” 是空节点(x为根节点),则将y设为根节点
     else if x = x.parent.left  
              then x.parent.left ← y    // 情况2:如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
     else x.parent.right ← y   // 情况3:(x是它父节点的右孩子) 将y设为“x的父节点的右孩子”
     y.left ← x             // 将 “x” 设为 “y的左孩子”
     x.parent ← y                // 将 “x的父节点” 设为 “y”
 }

注意 对子节点、父节点都要设置
红黑树R-B-tree_第3张图片

右旋

对y进行右旋,将y变成一个右节点。
红黑树R-B-tree_第4张图片

RIGHT-ROTATE(T, y)  
     x ← left[y]             // 前提:这里假设y的左孩子为x。下面开始正式操作
     left[y] ← right[x]      // 将 “x的右孩子” 设为 “y的左孩子”,即 将β设为y的左孩子
     p[right[x]] ← y         // 将 “y” 设为 “x的右孩子的父亲”,即 将β的父亲设为y
     p[x] ← p[y]             // 将 “y的父亲” 设为 “x的父亲”
     if p[y] = nil[T]       
         then root[T] ← x                 // 情况1:如果 “y的父亲” 是空节点,则将x设为根节点
     else if y = right[p[y]]  
               then right[p[y]] ← x   // 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的左孩子”
     else left[p[y]] ← x    // 情况3:(y是它父节点的左孩子) 将x设为“y的父节点的左孩子”
     right[x] ← y            // 将 “y” 设为 “x的右孩子”
     p[y] ← x                // 将 “y的父节点” 设为 “x”

红黑树R-B-tree_第5张图片

左旋 右旋 对比

红黑树R-B-tree_第6张图片

对x进行左旋,意味着,将“x的右孩子”设为“x的父亲节点”;即,将 x变成了一个左节点(x成了为z的左孩子)!。 因此,左旋中的“左”,意味着“被旋转的节点将变成一个左节点”。
对x进行右旋,意味着,将“x的左孩子”设为“x的父亲节点”;即,将 x变成了一个右节点(x成了为y的右孩子)! 因此,右旋中的“右”,意味着“被旋转的节点将变成一个右节点”。

红黑树的插入

要真正理解红黑树的插入,还得先理解二叉查找树的插入。磨刀不误砍柴工,咱们再来了解一下二叉查找树的插入和红黑树的插入。

如果要在二叉查找树中插入一个结点,首先要查找到结点要插入的位置,然后进行插入。假设插入的结点为z的话,插入的伪代码如下:

TREE-INSERT(T, z)  
    y ← NIL  
    x ← T.root  
    while x ≠ NIL  
        do y ←  x  
        if z.key < x.key  
            then x ← x.left  
        else x ← x.right  
    z.p ← y  
    if y == NIL  
        then T.root ← z         
    else if z.key < y.key  
        then y.left ← z  
    else y.right ← z 

红黑树的插入相当于在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作。

你可能感兴趣的:(算法,红黑树)