美文网首页
红黑树及源码的套路(中)

红黑树及源码的套路(中)

作者: 瑞瑞余之 | 来源:发表于2019-08-02 17:27 被阅读0次

    上文介绍了跟红黑树相关的知识点,尤其是左旋、右旋、自平衡等概念。本文开始结合源码对旋转和平衡进行深入分析。

    • HashMap为什么从链表换成了树
    • 从平衡二叉树到红黑树
    • 红黑树的左旋和右旋(源码解析)
    • 红黑树的插入自平衡(源码解析)
    • 红黑树的删除自平衡(源码解析)
    • HashMap中的红黑树(源码解析)

    红黑树的左旋和右旋(源码解析)

    1.红黑树节点的数据结构

    在开始之前,我们先看一下红黑树的数据结构

        static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
            TreeNode<K,V> parent;  // 父节点
            TreeNode<K,V> left; //左子节点
            TreeNode<K,V> right; //右子节点
            TreeNode<K,V> prev;    // 前节点
            boolean red; //是否为红色
        }
    

    我们注意到,TreeNode继承自LinkedHashMap.Entry<K,V>参看源码static class Entry<K,V> extends HashMap.Node<K,V> Entry又继承自HashMap.Node<K,V>,所以TreeNode也存在Node节点的成员变量,那么整体来看TreeNode的成员如下:

        static final class TreeNode<K,V> {
            TreeNode<K,V> parent;  // 父节点
            TreeNode<K,V> left; //左子节点
            TreeNode<K,V> right; //右子节点
            TreeNode<K,V> prev;    // 前节点
            boolean red; //是否为红色
            final int hash; 
            final K key;
            V value;
            Node<K,V> next;
        }
    

    2. 左旋

    上一篇文章已经介绍过左旋,我们来看一下示意图,挖掘其中的执行规律:


    左旋示意图

    左旋的时候,可以看作将pivot节点(18)向上拎起来,这样物理上原root节点(9)及root左子树下沉,变成pivot(18)的左子树,同时根据二叉搜索树的原则,pivot的左子树(10)会变成root节点(9)的右子树。
    可见如果我们要写rotateLeft函数,首先要告诉它pivot节点是谁,我们来看远源码:

            static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                                  TreeNode<K,V> p) {
                /*
                 root:整个树的root节点
                  p:pivot节点的parent节点(图中9)
                  r:parent的右节点(图中18),pp:parent的父节点(null),rl:pivot的左节点(图中10)
               */
                TreeNode<K,V> r, pp, rl; 
                if (p != null && (r = p.right) != null) {
                    if ((rl = p.right = r.left) != null)     //父节点(9)的右子节点指向pivot左子节点(10)
                        rl.parent = p;  //相应的设置pivot的左子节(10)的父节点为pivot的父节点(9)
                    if ((pp = r.parent = p.parent) == null)  //将pivot节点的父节点设置为之前的祖父节点
                        (root = r).red = false;  //如果pivot节点在的父节点为null则表示它是新的root节点,所以将它赋值给root变量,同时将root节点的颜色变为黑色
                    else if (pp.left == p) //如果pivot的父节点是祖节点的左子树
                        pp.left = r;  //那么让祖父节点的左子树从此指向pivot
                    else
                        pp.right = r;  //如果pivot的父节点是祖父觉点的右子树,则让祖父节点的右节点从此指向piovt
                    r.left = p;  //pivot左旋后成为新根节点,所以它的左子树指向原来的父节点
                    p.parent = r; //原来父节点的parent指向pivot
                }
                return root; //返回这棵树
            }
    

    通过阅读源码我们可以发现,实际的调整过程和我们上面分析的次序不同,在代码中是先将pivot节点的左子树移交到parent节点的右子树(双方互认父子);对parent的父节点进行判断,如果为空说明parent节点就是root节点,左旋后pivot即为新root节点,如果不为空就让pivot和祖父节点互认父子;最后pivot节点和原parent节点互认父子。

    3. 右旋

    右旋的逻辑和左旋近似。


    右旋
            static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                                   TreeNode<K,V> p) {
                /**
                      p:pivot的父节点(9)
                      l:pivot(7)
                     pp:pivot的祖父节点(null)
                     lr:pivot的右子树(8)
                */
                TreeNode<K,V> l, pp, lr;
                if (p != null && (l = p.left) != null) {  
                    if ((lr = p.left = l.right) != null)   
                        lr.parent = p;     //pivot右子树和pivot父节点互认父子
                    if ((pp = l.parent = p.parent) == null)   //如果pivot的祖父节点为空的时候,将pivot的父节点设为空
                        (root = l).red = false;   //将pivot设为root节点,且颜色为黑色
                    else if (pp.right == p)    //如果pivot的祖父节点存在,则pivot与祖父节点互认父子
                        pp.right = l;
                    else
                        pp.left = l;
                    l.right = p;   //pivot节点与原root节点互认父子
                    p.parent = l;
                }
                return root;
            }
    

    3. 插入后平衡

    插入后平衡指的是当进行插入操作后,因为新节点会默认为红色,所以可能违反红黑树原则而导致不平衡,需要通过自平衡算法进行再平衡。
    函数声明:

    static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                        TreeNode<K,V> x)
    

    root为整个树的根节点,x为插入的节点。首先我们看第一段代码:

                x.red = true;  //新插入的节点设置为红色
                for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {       //    ---point 1
                    if ((xp = x.parent) == null) {  //xp为插入节点的父节点
                        x.red = false;  //如果插入节点的父节点为空,意味着这是一个新树,插入的节点即为root节点,将其颜色变黑后,可直接返回
                        return x;
                    }
               /**如果父节点为黑色,那么插入一个红色节点自然无碍,
                  或者xpp作为祖父节点为空(这种情况作者没有想到何时
                  会出现,感觉写的有问题,似乎想表达的意思是x的父节
                  点就是根节点,既然这样,那父节点一定是黑色)*/
                    else if (!xp.red || (xpp = xp.parent) == null)   
                        return root;
    

    这一段代码其实就是针对插入的两种情况,一是插入的是一个空树,则改变x的颜色为黑即可,二是插入节点的父节点为黑色,则可以直接插入。这里的for循环定义时没有设置跳出条件,不难分析出,这里是在以新节点为起点,进行递归,递归的方式是不断更改x变量指向的节点,最后递归的结束条件就是我们看到的这两个return的条件。接下来,对于另外3种场景进行处理(上文已提到总共有5种场景)

    //进入这里说明父节点为红色(否则就直接return root了)
                    if (xp == (xppl = xpp.left)) {  //父节点是祖父节点的左子树
                        if ((xppr = xpp.right) != null && xppr.red) {  //祖父节点的右子树,也就是叔叔节点是红色
                            xppr.red = false;   //叔叔节点变黑
                            xp.red = false;  //父节点变黑
                            xpp.red = true;  //祖父节点变红
                            x = xpp;  让当前节点指向祖父节点
                        }
                        else {  
                            if (x == xp.right) {  //黑叔叔且当前节点是右子节点
                                root = rotateLeft(root, x = xp);  //将当前节点指向父节点,以父节点为轴心左旋
                                xpp = (xp = x.parent) == null ? null : xp.parent;   //此时xp就是新插入的节点了,xpp还是原来的祖父节点
                            }
                            if (xp != null) {
                                xp.red = false;  //将父节点置为黑色
                                if (xpp != null) {
                                    xpp.red = true;  //将爷爷节点置为红色
                                    root = rotateRight(root, xpp);  //以爷爷节点为轴心右旋
                                }
                            }
                        }
                    }
    

    可见如果右插入新节点发现它有个左边的红爸爸和右边的黑叔叔,则以红爸爸为轴心进行左旋,完成后父子的位置调换,将新爸爸和祖父节点的颜色改变,再进行右旋就完成平衡,整个过程图解如下:


    左右结构图解

    看完左爸爸和右叔叔,再看一下爸爸居右叔叔居左,也就是后半段代码:

                    else {
                        if (xppl != null && xppl.red) {  
    //和上面一样,如果父节点和叔叔节点都为红色,则红变黑,且祖父节点变红
                            xppl.red = false;
                            xp.red = false;
                            xpp.red = true;
                            x = xpp;
                        }
                        else {
    //如果是黑叔叔红爸爸,且爸爸是爷爷的右儿子,而我是爸爸的左儿子(右左结构)
                            if (x == xp.left) {
                                root = rotateRight(root, x = xp);  //儿子当爸爸,且为基点右旋
                                xpp = (xp = x.parent) == null ? null : xp.parent;  
                            }
                            if (xp != null) {
    //与上面一样,当树结构为右右结构后,变色,左旋
                                xp.red = false;
                                if (xpp != null) {
                                    xpp.red = true;
                                    root = rotateLeft(root, xpp);
                                }
                            }
                        }
                    }
    
    右左结构图解

    相关文章

      网友评论

          本文标题:红黑树及源码的套路(中)

          本文链接:https://www.haomeiwen.com/subject/oeitdctx.html