【Java源码阅读】TreeMap与红黑树

193 阅读8分钟

前言

  HashMap是一个有序的<K,V>结构,内部是由红黑树实现的。红黑树是一个平衡的二叉查找树,最坏的查找是件复杂度是O(logn)的,也就是说十亿条数据最差只需要30次比较,就能够找出来。

红黑树简单介绍

  阅读源码之前,需要对红黑树有个简单的了解。 红黑树的五个特性:

  • 节点是红色或黑色
  • 根节点是黑色
  • 每个红色节点的两个子节点都是黑色(两个红色结点不能相连)
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
  • 叶子结点(NIL)是黑色

红黑树保持平衡的方法有三种:变色、左旋、右旋。具体的平衡逻辑就不在这里细说了,有一篇非常好的文章建议阅读一下30张图带你彻底理解红黑树

TreeMap的基本方法

对结点p左旋

  • p的右结点变为它的父结点
  • p的右结点的左结点,成为p的右结点
private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
        //临时变量r=该结点的右结点
            Entry<K,V> r = p.right;
            //它右结点的左结点,变为该结点的右结点
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
             //以下操作使临时变量r变为了它的父结点
            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;
        }
    }

对结点p右旋

  • p的左结点变为p的父结点
  • p的左结点的右结点,成为该结点的左结点
private void rotateRight(Entry<K,V> p) {
        if (p != null) {
            //临时变量l=它的左结点
            Entry<K,V> l = p.left;
            //它的左结点,变为它的左结点的右结点
            p.left = l.right;
            if (l.right != null) l.right.parent = p;
            //以下操作使临时变量l变为了它的父结点
            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;
        }
    }

新增结点x后的平衡

首先,可以显而易见的看到,我们一开始默认插入的结点为红色,而因为红黑树的特性,在最后一行代码可以看到,根结点一定会变为黑色。 然后根据if-else操作,一共分为了7个场景 具体的就不列出来了,总结一下

  • 当父结点为黑结点时,并不会影响到平衡,所以无需做任何改变
  • 当父结点为红色时,根据规范,不可以有两个红色的结点相连,所以先要试试变色,叔叔结点与父结点颜色相同的话,只需变色即可
  • 当插入一个元素时,它的叔叔结点和父结点的颜色肯定相同,不然的话它之前就不符合红黑树的特性了,即根结点到任一叶子结点经过黑结点数量相同。那么只有一种可能性,叔叔结点为null。此时需要旋转来达到平衡。
  • 最后根结点一定要变成黑色!哪怕变成了一颗黑黑红树!
private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;
      
        while (x != null && x != root && x.parent.color == RED) {
        //若父结点是左结点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            //y是x的叔叔结点,也就是父结点的兄弟结点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                //当叔叔结点为红色
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                    //当叔叔结点为黑色
                } else {
                //当x为右结点时,需要多一步左旋操作
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    //变色、右旋
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
                //若父结点为右结点
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                //当叔叔结点为红色
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                    //当叔叔结点为黑色
                } else {
                //当x为左结点时,需要多一步右旋操作
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    //变色、左旋
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }

删除结点p

跟增加相比,删除结点比较复杂

  1. p没有子结点,直接删除即可
  2. p只有一个子结点,那么用子结点替换掉删除的结点
  3. p包含两个子结点,那么用后继结点(大于删除结点的最小结点)来替换 依据上述步骤找到替换结点之后,就要开始根据这个替换结点来做平衡操作了。
private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;
        
        //当p有俩结点的情况下
        if (p.left != null && p.right != null) {
        //找到后继结点,将删除结点的k-v换成后继结点,然后把后继结点之前的位置,当作待删除结点。由于后继结点一定没有左结点,可能存在右结点,所以可以在下面视作前两种场景处理。
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } 

        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        
        //p只有一个结点的情况下
        if (replacement != null) {
            // Link replacement to parent
            //p的左结点替换掉p
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;

            删掉p结点
            p.left = p.right = p.parent = null;

            //如果p是黑色,需要平衡操作
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { //如果我们只有这一个结点,直接删了返回就行
            root = null;
        } else {
        //没有子结点,直接删除,如果是黑色,需要进行平衡操作
            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;
            }
        }
    }

删除结点后的平衡操作

为了方便理解,下文中用D标记被删除结点、用R标记替换结点。 基于上段代码的理解,我们目前有个前提“被删除的结点是黑色”。 删除操作平衡场景分支较多,大致可以分为三种:靠自己、靠兄弟、靠父母。

  1. 靠自己 ---自己变黑
  • 若D无子结点且为红色,那么无需平衡。
  • 若D存在1个子结点,所以子结点即为R,R一定是红色的,无需平衡。
  • 若D存在2个子结点,那么找到它的后继结点,标记为R,此时R最多只有一个子结点,若R为红色无需平衡
  1. 靠兄弟 ---让兄弟变红
  • 若D无子结点且为黑色,且兄弟为黑色,那么兄弟只可能存在红色子结点,或者不存在结点。以下兄弟是右子结点为例,左子结点相反即可。
  • 若兄弟无子结点,直接让兄弟变红,父亲变黑,即可达到平衡
  • 若兄弟只存在左子结点,那么说明对父亲左旋后,会变瘸,故需要先对兄弟右旋。
  • 若兄弟存在右子结点,那么直接对父亲左旋即可平衡。
  • 若D存在两个结点、且R无子结点且为黑色,则将R当作D,操作跟上述一样。
  • 若D存在两个结点、且R有右结点且为黑色,那么R的右结点一定是红色,此时直接用R的子结点替换掉R即可平衡。
  1. 靠父母 ---让兄弟变红的前提下,让父亲变黑。
  • 如果D无子结点且为黑色,兄弟也没有结点且为黑色。那么则需将兄弟结点变为红色,且将父结点结点变为黑色
  • 如果父结点本来就是黑色,那就要让父结点去找它的兄弟了,以此类推
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));
               //如果兄弟结点是红色,兄弟变黑,父亲变红,左旋,此时兄弟的左结点变为了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));
                //兄弟是红色,兄弟变黑,父亲变红,右旋,此时兄弟的右结点是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);
    }

后记

红黑树这部分的知识还算是比较复杂的,总结一下

  • 插入或者删除一个红结点维持不变
  • 删除一个黑结点相当于把它从黑色变为红色,然后从下往上一层层的把红色赶出去,是一个自底向上的操作

本文只作为学习过程中的随笔,如果有不正确的地方欢迎指出。