Tree--RedBlackTree详解(2 - 3 - 4Tree)(红黑树)


  前言

  最近看到好多红黑树的东西,英文好的童鞋可以直接点击http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf这里查看我之前学习的材料,对理解下面讲的东西肯定也有点帮助(但是不完全一样),英文一般的同学就直接看我的文采飞扬把哈哈。还有大家可以去coursera上学习一些国外比较好的资料。感觉比国内一些学习网站做的好很多。

  前面一篇随笔写的binarysearchtree(http://www.cnblogs.com/robsann/p/7567596.html)说了有一个缺点就是不平衡,意思就是插入已经排好序的对象的时候会变成一条链表,链表当然比二叉树要慢很多啦,随机插入的话二叉树的各种方法需要的时间和LgN的成正比。所以红黑树其实是在解决二叉树不平衡的问题的。

  度娘(这里稍微看一下)

  红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:   性质1. 节点是红色或黑色。   性质2. 根节点是黑色。   性质3 每个叶节点(NIL节点,空节点)是黑色的。   性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)   性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。   这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

 正文 

  2 - 3 - 4 Tree

  2 - 3 - 4 Tree 我觉得算是一种模型,一种树模型保证了树是平衡的,所谓平衡就是树不会一个枝头长得很高,另外一个枝头长得很矮,那保证平衡有什么用?平衡的情况下,所有操作需要的时间都是和LgN成正比的,你说腻害不腻害。

  2 - 3 - 4 树,允许一个节点是 2-nodes 或者是 3 nodes 或者是 4 nodes, 具体的意思就是说

   这是一个2-nodes, 有2个触手,能够指向不同的2个子元素,左边的子元素小于A,右边的子元素大于A

  3-nodes, 3 分叉,最左边的子元素小于C,中间的子元素between c和e, 最右边的子元素大于E

    4-nodes, 3分叉。同上

  看看2-3-4tree是如何保证平衡的

  假设有 10 7 6 3 8 11 15 要插入 2 -3 4 树中

  这里插入还有另外一种选择叫Bottom-up solution,之下而上的一种解法(可以忽略)。就是先找到这个节点会被插入的位置,如果插入后变成了5-nodes,就把其中一个节点往父节点抛出,让父节点和其中的一个子节点结合。

  删除同理,为了要保证树的平衡,当删除的时候,如果被删除的节点只有一个元素的话,必须要把父亲元素拉下来形成一个3-nodes然后删除后变成2-nodes(后面还会说)

  左倾红黑树(LeftLeaningRedBlackTree)

  红黑树和2-3-4树的关系,红黑树是2-3-4树的一种实现方式。2-3-4树只是一个模型。

  先介绍一下节点对象Node,看代码,之后会用到这个对象

    private class Node {
        private K k; // 这个节点的key 
        private V v; //节点的value
        private Node left, right;  //左右节点
        private int size;  //节点为根的树的大小
        private boolean color; //节点的颜色,分黑和红
        
        Node(K k, V v, int size, boolean color) {
            this.k = k;
            this.v = v;
            this.size = size;
            this.color = color;
        }
        
        @Override
        public String toString() {
            if (color) return "red " + v;
            else return "black " + v;
        }
        
    }
Node(节点对象)

  左倾红黑树和红黑树的区别如下

  这是一般的红黑树,3-node 的时候红节点可以左倾或者右倾

  左边是2-3-4树的模型,右边是红黑树的实现

  这是左倾红黑树, 3-nodes的时候红色的节点必须在左边。

  用常量来表示红黑

  红黑树的修正

  首先左倾红黑树的性质必须要得到保证(就是上图中说的Require that 3 -node be left leaning),但是很多时候可能因为插入或者删除的操作破坏了这个性质,所以我们必须要修正。

  这里介绍3个修正的方法

  第一个方法的使用场景是这样子的,(为了不破坏平衡,每次插入的都是红节点),下图插入的节点在右边,破坏了左倾的性质,所以必须rotateLeft。rotateLeft是指把红色节点左移

  方法如下

    //右树是红link的时候,turn this red link to left
    private Node rotateLeft(Node  h) {
        assert(isRed(h.right));
        
        Node  x = h.right;  //change the pointers
        h.right = x.left;
        x.left = h;
        
        x.color = h.color; //change the colors
        h.color = RED;
        
        x.size = h.size; //change the sizes
        h.size = size(h.left) + size(h.right) + 1;
        return x;
    }
rotateLeft(仔细看一边)

  另外一种情况是最新插入的节点在最左边,把中间节点rotateRight,重新平衡

  方法如下

    //左树是红link的时候,turn this red link to left
    private Node rotateRight(Node  h) {
        assert(isRed(h.left));
        
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        
        x.color = h.color;
        h.color = RED;
        
        x.size = h.size; //size is the same
        h.size = size(h.left) + size(h.right) + 1;
        
        return x;
    }
rotateRight

   这张图是破坏了性质之后,修正的办法

   

  还有一个方法是flipColors(),代码如下,就是可能插入的时候需要满足当前节点不是4-nodes,可能就会使用这个方法

  有了上面这些辅助的方法后就可以开始下面的学习了

  左倾红黑树的put  

  在2-3-4tree中的介绍中也知道了,put的时候,当前节点如果是4-nodes的话就没有位置留给需要插入的对象了。

  所以我们在put的时候,一定要保证当前的节点(currentNode)以后用cn来表示。cn必须不是4-nodes, 如果是4-nodes的话就用flipColor把4-node变成3个2-node

  假设我们要插入10  7  6  3 这4个对象, 大片动态图,燃烧的经费。

  附上代码

    public void put(K k, V v) {
        root = put(root, k, v);
        root.color = BLACK;
    }
    
    private Node put(Node cn, K k, V v) {
        if (cn == null) return new Node(k, v, 1, RED);
        if(isRed(cn.left) && isRed(cn.right)) split4Node(cn);//是4节点的话 就split
        
        int cmp = k.compareTo(cn.k);
        if (cmp > 0) cn.right = put(cn.right, k, v); // k > node.k go right
        else if (cmp < 0) cn.left = put(cn.left, k, v);
        else cn.v = v; //hit
        
        //following code is to fix the tree on the way up
        if (isRed(cn.right) && !isRed(cn.left)) cn = rotateLeft(cn); // right leaning 3nodes的时候   需要变成 left leaning
        if (isRed(cn.left) && isRed(cn.left.left)) cn = rotateRight(cn);  //变成了一个4节点
        
        cn.size = size(cn.left) + size(cn.right) + 1;
        return cn;
    }
put

  左倾红黑树的get

  树的get方法其实很简单,就是判断key是不是相等,如果相等就return 这个值。

    public V get(K k) {
        return get(root, k);
    }
    
    //cn means currentNode
    private V get(Node cn, K k) {
        if (cn == null) return null; // not find the key
        
        int cmp = k.compareTo(cn.k);
        if(cmp > 0) return get(cn.right, k);
        else if (cmp < 0) return get(cn.left, k);
        else return cn.v; // hit
    }
get

  左倾红黑树的删除

  删除可以说是最难的了把,基本的思想就是,cn节点(当前节点)不会是2-node,(如果root节点是2-nodes,我们需要把root节点变成红节点)

  保证其中一个子节点不是2节点(这个保证需要看删除的节点位于当前节点的哪里,比如删除的节点比cn节点小,所以接下来我们会往left走,所以要保证left节点不是2-node)。因为2节点如果删除了的话就不会平衡。所以必须要把红节点从root一步一步carry下去。

  先实现一个deleteMin方法,我们要把红节点带向左边。想一想,红节点带向左边后,如果左边有节点删除了可能没办法保持平衡,红节点可以变成黑节点,代替刚才被删除的节点。通过这样子可以保证左边的树是一定会平衡的。

  deleteMin的几种情况

  1. , 现在可以直接删除掉。也不会影响平衡。删除了后3节点变成了2节点。

  2., 需要继续向左走,但是左子节点是2-node,必须要想办法变成不是2-node。所以需要把父亲节点和兄弟节点和cn节点。整合在一起变成4-node

  3. , 需要继续向左走,但是左子节点是2-node,必须要想办法变成不是2-node。发现兄弟节点是不是2-nodes。所以把兄弟节点借一个node过来

  2和3这2种情况总结在一起就是moveRedLeft的代码

    public void deleteMin() {
        //保证了root节点不是2nodes
        if (!isRed(root.left) && !isRed(root.right))
            root.color = RED;
        
        root = deleteMin(root);
        root.color = BLACK;
    }
    
    
    public Node deleteMin(Node cn) {
        if (cn.left == null) return null;
        
        if (!isRed(cn.left) && !isRed(cn.left.left))  //判断左边子节点是不是2node,是的话就需要把Red带下去
            cn = moveRedLeft(cn);
        
        cn.left = deleteMin(cn.left);
        
        return fixup(cn);
    }

    private Node fixup(Node h) {
        
        if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h); //右倾
        if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
        h.size = size(h.left) + size(h.right) + 1; //right the size
        return h;
    }
deleteMin

   随意的delete方法。按照下面的图说一下,基本的思想。

  首先删除D。从H开始,H不是2-node(右边有一个红节点),D小于H,左往H的左边找

  找到了D。D不是2-node且是红节点。找到了D,处理办法是找到右树中最小的值,发现是E, 把最小的值赋值给当前的node

  所以我们要往右边走。但是发现右边节点F是2-node。所以我们要把红色的连接往右边带。

  flipColor(D), B D 和 F就变成了一个4-node(可以自己做一下图看看是不是这样子的)。 这时候红链接也往右边带了。

  现在到了F 节点。发现F 的左节点是 2-node。把红色的链接往左边带。

  flipColor(F),这个时候F 和 E 和 G ,变成了一个4-node。 顺理成章删除左边的节点。没有影响平衡。

  接着原路返回,修复节点。

    public void delete(K k) {
        if (k == null) throw new IllegalArgumentException("argument to delete() is null");
        if (!contains(k)) return;
        
        if (!isRed(root.left) && !isRed(root.right))
            root.color = RED;
        root = delete(root, k);
        if (root != null)
            root.color = BLACK;
    }
    
    public boolean contains(K k) {
        return get(k) != null;
    }

    private Node delete(Node cn, K k) {
        
        if (cn == null) return null;
        
        int cmp = k.compareTo(cn.k);
        
        if (cmp < 0) { // k < node.k go left
            if (!isRed(cn.left) && !isRed(cn.left.left)) //保证了下一个左元素不是2nodes
                cn = moveRedLeft(cn);
            cn.left = delete(cn.left, k);
        } else if (cmp > 0) { // k > node.k go right
            if (isRed(cn.left) && !isRed(cn.right)) //如果是3节点的话需要 rotate 把red转到右边
                cn = rotateRight(cn);
            if (!isRed(cn.right) && !isRed(cn.right.left)) //保证下一个右节点不是2nodes
                cn = moveRedRight(cn);
            cn.right = delete(cn.right, k);
        } else { //hit
            
            if (isRed(cn.left) && !isRed(cn.right)) 
                cn = rotateRight(cn);
            
            if (k.compareTo(cn.k) == 0 && (cn.right == null)) //find null just return null
                return null;
            
            if (!isRed(cn.right) && !isRed(cn.right.left)) //保证下一个右节点不是2nodes
                cn = moveRedRight(cn);
            
            if (k.compareTo(cn.k) == 0) {
                 Node x = min(cn.right);
                 cn.k = x.k;
                 cn.v = x.v;
                 cn.right = deleteMin(cn.right);
            } else cn.right = delete(cn.right, k);
        }
        return fixup(cn);
    }
delete

  总结 

  具体的实现可以参考一下https://github.com/Cheemion/algorithms/blob/master/src/com/algorithms/tree/LeftLeaningRedBlackTree.java

  可能有地方说的不清楚哈。见谅,可以留言我有不清楚的地方