3

虽是读书笔记,但是如转载请注明出处 http://segmentfault.com/blog/exploring/
.. 拒绝伸手复制党

关于二叉树的基本知识,可以参见:Java 实现基本数据结构 2(树)

以下是算法导论第13章的学习笔记


红黑树

BST的各种操作的时间复杂度是依赖于树的高度,通过使得BST成为红黑树,确保每次对BST进行插入和删除之后,树的高度上限依然是logn.

红黑树,本质上来说就是一棵二叉查找树,而且是平衡的查找二叉树 -- 让BST效率更优

定义

红黑树中每个结点包含五个域:color,key,left,rightp。通过对一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长两倍。

如果某结点没有一个子结点或父结点,则该域指向 NIL。

我们把 NIL 视为二叉树的外结点 (叶子),而带关键字的结点视为内结点。

一棵二叉树如果满足下面的红黑性质,则为一棵红黑树:

1) 每个结点或是红的,或是黑的。

2) 根结点是黑的。

3) 每个叶结点 (NIL) 是黑的。

4) 如果一个结点是红的,则它的两个儿子都是黑的。

5) 对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。
Black-Red

采用哨兵来代表 NIL,它的 color 域为 BLACK,其它域为任意值。

从某个结点 x 出发 (不包括该结点) 到达一个叶结点的任意一条路径上,黑色结点的个数称为该结点x黑高度,用bh(x) 表示。

引理:一颗有 n 个内结点的红黑树的高度至多为 2lg(n+1)。


操作

旋转

旋转的目的是让树保持红黑树的特性。

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

图片描述

图片描述

        // 左旋x
    public void rotate(TreeNode root, TreeNode x){
        if(x.right != null){
            //处理x的右孩子
            TreeNode y = x.right;
            x.right = y.left;
            if(y.left != null)
                y.left.parent = x;
            // 处理x的父节点
            y.parent = x.parent ;
            if(x.parent != null){
                // 判断y链接的位置
                if(x.parent.left == x){
                    x.parent.left = y;
                }
                if(x.parent.right == x){
                    x.parent.right = y;
                }
            }else{
                root = y;
            }
            // 链接新的父节点
            x.parent = y;
            y.left = x;
        }
    }

Note: 右旋转的时候可以把代码中的left换成right就好了。

插入

关于插入和删除整理自July大神的blog和youtube的短视频
youtube

重温下RedBlack tree的五条性质:
1 节点 r or b
2 根 b
3 叶子 b
4 红色节点孩子必为黑
5 任意节点其叶子节点的路径包含相同个数黑节点

红黑树插入过程的思想是:利用BST的插入方法,寻找待插入元素的位置并插入[所以这一部分可以把BST的直接挪过来]。然后(sth different:) 将待插入元素涂红色。为了保证红黑树的五条性质,需要调用辅助程序rbInsertFixup来调整,对节点重新着色并旋转。

插入情况(插入的节点p设置为红)有三:
1. 原tree为空树,所以p设置为根节点 -- 解决方案:Just 设置p为黑就可以
2. 插入节点的父节点为 -- 无需解决方法,插入后无影响。
3. ** 插入节点的父节点为 -- 需要rbInsertFixup
case 1: p节点和叔节点都为 -- 解决方案:父+叔 都涂;祖父涂p = 祖父从新的当前结点重新开始算法
case 2: p节点为,且p是父节点的子 -- 解决方案:p = 父, 左旋p
(case2 实际上有两种,看youtube 视频时候才发现)图片描述
(这两种情况可以想象成一个菱形的两半。只要右子就左旋,左子就右旋)
case 3: p节点为,且p是父节点的子 -- 解决方案:父+叔 都涂, 父节点涂,祖父涂红,祖父右旋。
(case3 实际上也有两种,这两种情况可以想象成两条直线,三角形除了底的两条边)

上面三种情况 (Case) 处理问题的核心思路都是:将红色的节点移到根节点;然后,将根节点设为黑色。

case2 和 case3 的区分是1. 二者二叉树的结构不同,菱形和三角 2. 解决方案不同,涂黑or not 图片描述
最后,把根结点涂为色,整棵红黑树便重新恢复了平衡。

        //插入
        public RBNode insert(RBNode root, RBNode x){
        RBNode y = this.Nil; // Nil
        RBNode p = root;
        // if the node inserted is null
        if(x == null){
            return root;
        }
        // seek the place where x to be inserted
        while(p!=null){         
            if(x.val > p.val){
                y = p;
                p = p.right;
            }
            if(x.val < p.val){
                y = p;
                p = p.left;
            }
        }
        // insert
        if(y == Nil){
            root = x;
        }
        else
        {
            x.parent = y;
            if(x.val > y.val){
                y.right = x;
            }
            else{
                y.left = x;
            }
        }
        // something different from BST insert 
        x.left = Nil;
        x.right = Nil;
        x.color = 0; // set it red;
        // fixup
        rbInsertFixup(root, x);
        return root;
    }
        public void rbInsertFixup(RBNode root, RBNode x){
        // the fixup occurs when x.partent is red
        while(x.parent.color == 0){
            // 又分为父结点是祖父结点的左子还是右子,对于对称性,我们只要解开一个方向就可以了
            if(x.parent == x.parent.parent.left){
                RBNode uncle = x.parent.parent.right;
                // case 1 
                if(uncle.color == 0){
                    x.parent.color = 1;
                    uncle.color = 1; 
                    x.parent.parent.color = 0;
                    x = x.parent.parent;
                }

                else
                {
                    // case 2
                    if(x == x.parent.right){
                    {   
                        x = x.parent;
                        this.rotateLeft(root, x);
                    }
                    // case 3
                    {
                        x.parent.color = 1;
                        x.parent.parent.color = 0;
                        this.rotateRight(root, x.parent.parent);
                    }
                }
            else
            {
                    // same as the clause with right and left child
            }
                    }
                }
            root.color = 1;
            }
    }

删除

摘录整理自blog
将红黑树内的某一个节点删除。需要执行的操作依次是:
首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;
然后,通过 "旋转和重新着色" 等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:

第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和 "删除常规二叉查找树中删除节点的方法是一样的"。分 3 种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就 OK 了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把 “它的后继节点的内容” 复制给 “该节点的内容”;之后,删除 “它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给 "被删除节点" 之后,再将后继节点删除。这样就巧妙的将问题转换为 "删除后继节点" 的情况了,下面就考虑后继节点。 在 "被删除节点" 有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然 "的后继节点" 不可能双子都非空,就意味着 "该节点的后继节点" 要么没有儿子,要么只有一个儿子。若没有儿子,则按 "情况①" 进行处理;若只有一个儿子,则按 "情况②" 进行处理。

第二步:通过 "旋转和重新着色" 等一系列来修正该树,使之重新成为一棵红黑树。
因为 "第一步" 中删除节点之后,可能会违背红黑树的特性。所以需要通过 "旋转和重新着色" 来修正该树,使之重新成为一棵红黑树。

图片描述

性能

-- BST 红黑树 Btree
遍历 O(n)
插入 O(h) O(h) 4
删除 O(h) O(h) 1
查询 O(h) O(h) 2
最小(大) O(h) O(h) 2
后继(前驱) O(h) O(h)
旋转 - O(1) -

用途

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


和AVL比较

AVL比RBtree更加平衡,但是AVL的插入和删除会带来大量的旋转。
所以如果插入和删除比较多的情况,应该使用RBtree, 如果查询操作比较多,应该使用AVL.



SecondLife
1.9k 声望252 粉丝