zoukankan      html  css  js  c++  java
  • 红黑树

    红黑树

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

    2、树的根始终是黑色的

    3、没有两个相邻的红色节点(红色节点不能有红色父节点或红色子节点,并没有说不能出现连续的黑色节点)

    4、从节点(包括根)到其任何后代Null节点(叶子节点下方挂的两个空节点,并且认为它们是黑色的)的每条路径都具有相同数量的黑色节点。

    再向红黑树里面添加元素的时候,红黑树主要是通过下面这两个操作来保持平衡的:

    1、 recolor(重新标记 黑色或红色)

    2、 rotation(旋转)

    首先是 recolor,如果重新标记不能够达到红黑树的4点要求。那么我们就需要使用rotation。

    假设我们插入的新节点为 X

    1. 将新插入的节点标记为红色

    2. 如果 X 是根节点(root),则标记为黑色

    3. 如果 X 也不是root 同时X 的parent不是黑色 ;

      3.1、如果 X 的uncle(叔叔) 是红色

      1. 将parent 和uncle标记为黑色
      2. 将 grand parent(祖父)标记为红色
      3. X节点的颜色和X祖父的颜色相同,然后重复步骤2,3

      来看下图

      按照上面的公式来:

      ​ 1、 将新插入的X节点标记为红色

      ​ 2、发现 X的parent(P)同样为红色,这违反了上面的第三条原则[没有两个相邻的红色节点]

      ​ 3、发现X的 uncle(U)同样为红色

      ​ 4、 将P和U 标记为黑色

      ​ 5、将 XX的grand parent(G)标记为相同的颜色,即红色,继续重复公式2、3

      ​ 6、发现G是根节点,标记为黑色

      ​ 7、 结束

      3.2 如果X 的uncle(叔叔)是黑色,就会有下面四种情况

      ​ 1、 左左(P 是 G 的左孩子,并且 X是 P 的左孩子)

      ​ 2、 左右(P 是 G 的左孩子,并且 X是 P 的右孩子)

      ​ 3、 右右(P 是 G 的右孩子,并且 X是 P 的右孩子)

      ​ 4、 右左(P 是 G 的右孩子,并且 X是 P 的左孩子)

      先简单的演示一下怎么旋转的

      如下图的红黑树,我们插入的节点是65

      左左节点旋转(j插入节点的父节点是左节点,插入节点也是左节点):

      ​ 可以围绕祖父节点69右旋,再结合变色,步骤如下

      左右节点旋转(插入节点的父节点是左节点,插入节点是右节点)

      ​ 可以先围绕父节点66左旋,然后再围绕祖父节点69右旋,最后再将67设置为黑色,把69设置为红色

      通过上面这两个可以发现如果要插入的节点是在右节点,它比左节点多了一步 父节点左旋的操作

      如果插入的数据是在右节点,如下图,要插入节点68

      右左节点旋转(插入节点的父节点是右节点,插入节点也是左节点)

      ​ 可以先围绕父节点69右旋,接着在围绕祖父节点66左旋,最后把68节点设置为黑色,把66设置为红色。

      右右节点旋转(插入节点的父节点是右节点,插入节点也是右节点)

      可以围绕祖父节点66左旋,再把旋转后的根节点69设置为黑色,把66这个节点设置为红色

      通过上面两个发现插入的节点为左节点的话,比右节点多了一步父节点右旋的操作

    红黑树在Java中的实现

     private static final boolean RED   = false;
     private static final boolean BLACK = true;
    
    // TreeMap里面使用 Entry来保存节点的数据
     static final class Entry<K,V> implements Map.Entry<K,V> {
            K key;
            V value;
            Entry<K,V> left;
            Entry<K,V> right;
            Entry<K,V> parent;
            boolean color = BLACK;
            
            Entry(K key, V value, Entry<K,V> parent) {
                this.key = key;
                this.value = value;
                this.parent = parent;
            }
            
            .....
    }
    

    接下来看下 TreeMap的put方法

      public V put(K key, V value) {
      		//先用t来保存 root节点的数据
            Entry<K,V> t = root;
            //判断当前链表是否为空
            if (t == null) {
                compare(key, key); // type (and possibly null) check
    
                root = new Entry<>(key, value, null);
                size = 1;
                // 记录修改次数加1
                modCount++;
                return null;
            }
            int cmp;
            Entry<K,V> parent;
            // split comparator and comparable paths
            // 这个值在构造函数的时候赋值
            Comparator<? super K> cpr = comparator;
            if (cpr != null) {
                do {
                    parent = t;
                    //将新插入的key和t的key进行比较
                    cmp = cpr.compare(key, t.key);
                    //如果新插入的key小于t.key,t等于t左边的节点
                    if (cmp < 0)
                        t = t.left;
                    //如果新插入的key大于t.key,t等于t的右边节点
                    else if (cmp > 0)
                        t = t.right;
                    else
                    	//如果两个key相等,新的value覆盖原有的value,并返回原有的value
                        return t.setValue(value);
                } while (t != null);
            }
            else {
                if (key == null)
                    throw new NullPointerException();
                @SuppressWarnings("unchecked")
                    Comparable<? super K> k = (Comparable<? super K>) key;
                do {
                    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);
            }
            //将新插入的节点作为parent节点的子节点
            Entry<K,V> e = new Entry<>(key, value, parent);
            //如果新插入key小于parent的key,则e作为parent的左子节点
            if (cmp < 0)
                parent.left = e;
              //如果新插入key小于parent的key,则e作为parent的右子节点   
            else
                parent.right = e;
            //修复红黑树
            fixAfterInsertion(e);
            size++;
            modCount++;
            return null;
        }
    

    查看红黑树是如何修复的

    private void fixAfterInsertion(Entry<K,V> x) {
    		// 默认给红色的
            x.color = RED;
    		// X节点的父节点不是根节点,并且父节点的颜色是红色,如果父节点是黑色,那就不需要修复
            while (x != null && x != root && x.parent.color == RED) {
                // 如果 x的父节点是 x 祖父节点的 左节点
                if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                	// 获取 x的 祖父节点的右节点,也就是叔叔节点
                    Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                    // 如果x的 叔叔节点是红色
                    if (colorOf(y) == RED) {
                    	//将X的父节点设置为黑色
                        setColor(parentOf(x), BLACK);
                        //将x 的叔叔节点设置为黑色
                        setColor(y, BLACK);
                        // 将x 的祖父节点设置为红色
                        setColor(parentOf(parentOf(x)), RED);
                        x = parentOf(parentOf(x));
                        
                        //如果x的叔叔节点是黑色
                    } else {
                    	// 对应的是 左右节点旋转
                    	// 如果x是其父节点的右节点
                        if (x == rightOf(parentOf(x))) {
                        	// 将x的父节点设置为x
                            x = parentOf(x);
                            //左旋转
                            rotateLeft(x);
                        }
                        //把x的父节点设置为黑色
                        setColor(parentOf(x), BLACK);
                        // 把x的祖父节点设置为红色
                        setColor(parentOf(parentOf(x)), RED);
                        //右旋转
                        rotateRight(parentOf(parentOf(x)));
                    }
                    // 如果 X 的父节点是其祖父节点的右节点
                } else {
                	// 获取X 的父节点的兄弟节点即 叔叔节点
                    Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                    // 只着色的情况对应的是最开始的例子,没有旋转操作,但要对应多次变换
                    //如果x的父节点的兄弟节点是红色
                    if (colorOf(y) == RED) {
                    	//将 x 的父节点设置为黑色
                        setColor(parentOf(x), BLACK);
                        //将x的叔叔节点设置为黑色
                        setColor(y, BLACK);
                        //将x的祖父节点设置为红色
                        setColor(parentOf(parentOf(x)), RED);
                        x = parentOf(parentOf(x));
                    } else {// 如果x 的叔叔节点是黑色
                    
                    	//如果x是父节点的左节点
                        if (x == leftOf(parentOf(x))) {
                            // 将x 的父节点设为x
                            x = parentOf(x);
                            rotateRight(x);
                        }
                        //将x的父节点设为黑色
                        setColor(parentOf(x), BLACK);
                        //把x的祖父节点设为红色
                        setColor(parentOf(parentOf(x)), RED);
                        rotateLeft(parentOf(parentOf(x)));
                    }
                }
            }
            //将根节点强制设为黑色
            root.color = BLACK;
        }
    

    ​ TreeMap的插入节点和普通的排序二叉树的区别在于 TreeMap插入节点之后会调用方法

    fixAfterInsertion(e)来重新调整红黑树的结构来让红黑树保持平衡

    ​ 下面可以来看下fixAfterInsertino(e)方法的执行流程

    第一种:只需变色就可保持平衡

    在下面的这个树中插入节点51

    当我们插入节点51之后,TreeMap的put方法执行后会得到下面的这张图

    接着调用 fixAfterInsertion(e)方法,代码流程如下

    当第一次进入循环之后,执行后会得到下面的红黑树结构

    再把x重新赋值之后,重新进入while循环,此时x节点为45

    执行上述流程后,得到下面所示的红黑树结构

    这个时候 X被重新赋值为69,因为60是根节点,所以会退出while循环。在退出循环后,会再次把根节点设置为黑色,得到最终的结构如下图

    最后经过两次执行while循环后,我们的红黑树会调整成现在这样的结构,这样的红黑树结构是平衡的,所以路径的黑高一致,并且没有红色节点相连的情况。

    第二种: 旋转搭配变色来保持平衡

    给定下面这样一颗红黑树

    现在插入节点66,得到如下的树结构

    之后进入fixAfterInsertion(e)方法

    最终得到的红黑树的机构如下

    这样红黑树就保持平衡了

    红黑树的删除

    ​ 删除的时候需要考虑这个节点位于哪个位置,是否有左右节点

    删除节点是根节点

    ​ 直接删除根节点即可

    删除节点的左孩子和右孩子都为空

    ​ 直接删除当前节点即可

    删除节点有一个子节点不为空

    ​ 这时候需要使用子节点来代替当前需要删除的节点,之后再把子节点删除即可

    ​ 如下图,当我们需要删除节点69的时候

    首先用子节点代替当前删除节点,然后再把子节点删除

    最终的红黑树结构如下面所示,这个结构的红黑树我们是不需要通过变色+旋转来保持红黑树的平衡了,因为将子节点删除后树已经是平衡的了。

    还有一种场景是当我们待删除节点是黑色的,黑色的节点被删除后,树的黑高就会出现不一致的情况,这个时候就需要重新调整结构。

    还是拿上面这颗删除节点后的红黑树举例,我们现在需要删除节点67。

    因为67 这个节点的两个子节点都是null,所以直接删除,得到如下图所示结构:

    这个时候我们树的黑高是不一致的,左边黑高是3,右边是2,所以我们需要把64节点设置为红色来保持平衡。

    删除节点的两个子节点都不为空

    ​ 删除节点两个自及诶单都不为空的情况下。跟上面有一个节点不为空的情况也是有些类似。

    同样是需要找到能代替当前节点的节点,找到后,把能替代删除节点值复制过来,然后再把替代节点删除掉

    • 先找到替代节点,也就是前驱结点后者后继节点
    • 然后再把前驱节点或者后继节点复制到当前待删除节点的位置,然后在删除前驱结点或者后继节点。

    ​ 前驱节点:对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的前一个节点为该节点的前驱节点;

    ​ 后继节点:对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的后一个节点为该节点的后继节点;

    ​ 简单点:前驱是左子树中最大的节点,后继则是右子树中最小的节点。

    ​ 如上图的的二叉树按照中序遍历为(43,45,49,56,58,60,64,66,68,72)那么 60的前驱结点是58后继节点是64

    前驱或者后继都是最接近当前节点的节点,当我们需要删除当前节点的时候,也就是找到能替代当前节点的节点。
    

    第一种,前驱结点为黑色节点,同时有一个非空节点

    如下面一棵树,我们需要删除节点64

    首先找到前驱结点,把前驱结点复制到当前节点

    接着删除当前前驱结点

    这个时候60和63这两个节点都是红色的,我们尝试把60这个节点设置为黑色即可使整个红黑树达到平衡。

    第二种,前驱结点为黑色节点,同时子节点都为空

    前驱结点是黑色的,子节点都为空,这个时候操作步骤与上面基本类似

    操作步骤如下

    ​ 因为要删除节点64,接着找到前驱节点63,把63节点复制到当前位置,然后将前驱节点63删除掉,变色后出现黑高不一致的情况下,最后把63节点设置为黑色,把65节点设置为红色,这样就能保证红黑树的平衡。

    第三种,前驱节点为红色节点,同时子节点都为空

    给定下面这颗红黑树,我们需要删除节点64的时候。

    同样的,我们知道64的前驱结点63,接着把63赋值到64这个位置

    然后删除前驱结点

    删除节点后不需要变色也不需要旋转即可保持树的平衡。

    public V remove(Object key) {
    		//这个是获取这个key对应的entry
            Entry<K,V> p = getEntry(key);
            if (p == null)
                return null;
    		
            V oldValue = p.value;
            //删除这个节点,
            deleteEntry(p);
            return oldValue;
      }
    

    deleteEntry(e)可以删除一个节点,并且保持树的平衡

    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.
            //目标节点的左右子树不为空
            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.
            //要删除的节点只有一个子节点为空
            Entry<K,V> replacement = (p.left != null ? p.left : p.right);
            if (replacement != null) {
                // Link replacement to parent
                replacement.parent = p.parent;
              //如果p是根节点
                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
                if (p.color == BLACK)
                	//调整
                    fixAfterDeletion(replacement);
            } else if (p.parent == null) { // return if we are the only node.
                root = null;
            } else { //  No children. Use self as phantom replacement and unlink.
               //目标节点的左右子树都为空
               if (p.color == BLACK)
                    fixAfterDeletion(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;
                }
            }
        }
    

    successor() 寻找节点的后继函数

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
            if (t == null)
                return null;
                // t.右子树不空,则t的后继节点是其右子树中最小的那个元素
            else if (t.right != null) {
                Entry<K,V> p = t.right;
                while (p.left != null)
                    p = p.left;
                return p;
            } else {
            	// t 的右孩子为空,则t的后继是其第一个向左走的祖先
                Entry<K,V> p = t.parent;
                Entry<K,V> ch = t;
                while (p != null && ch == p.right) {
                    ch = p;
                    p = p.parent;
                }
                return p;
            }
        }
    

    fixAfterDeletion() 调整位置、颜色

    private void fixAfterDeletion(Entry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                Entry<K,V> sib = rightOf(parentOf(x));
    
                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateLeft(parentOf(x));
                    sib = rightOf(parentOf(x));
                }
    
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    if (colorOf(rightOf(sib)) == BLACK) {
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        sib = rightOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } 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);
    }
    

    左转、右转

    /** From CLR */
        private void rotateLeft(Entry<K,V> p) {
            if (p != null) {
                Entry<K,V> r = p.right;
                p.right = r.left;
                if (r.left != null)
                    r.left.parent = p;
                r.parent = p.parent;
                if (p.parent == null)
                    root = r;
                else if (p.parent.left == p)
                    p.parent.left = r;
                else
                    p.parent.right = r;
                r.left = p;
                p.parent = r;
            }
        }
    
        /** From CLR */
        private void rotateRight(Entry<K,V> p) {
            if (p != null) {
                Entry<K,V> l = p.left;
                p.left = l.right;
                if (l.right != null) l.right.parent = p;
                l.parent = p.parent;
                if (p.parent == null)
                    root = l;
                else if (p.parent.right == p)
                    p.parent.right = l;
                else p.parent.left = l;
                l.right = p;
                p.parent = l;
            }
        }
    

    史上最清晰的红黑树讲解(下
    我画了近百张图来理解红黑树

  • 相关阅读:
    Linux makefile教程之概述一[转]
    Valid Parentheses
    Letter Combinations of a Phone Number
    机器学习经典分类算法 —— C4.5算法(附python实现代码)
    3Sum Closest
    3Sum
    Integer to Roman
    寒假文献阅读(四)
    Longest Common Prefix
    Roman to Integer
  • 原文地址:https://www.cnblogs.com/zhuguangzhe/p/13219096.html
Copyright © 2011-2022 走看看