《码出高效》学习:TreeMap与红黑树

首先先看TreeMap的继承关系:

继承了抽象类AbstractMap,实现了NavigableMap(SortedMap)、Cloneable、Serializable三个接口

  • NavigableMap(SortedMap):使Key有序,可以获取头尾K-V对,或者获取指定范围内的SubMap
  • Cloneable:支持clone方法
  • Serializable:支持序列化

基于红黑树实现:

    // Red-black mechanics

    private static final boolean RED   = false;
    private static final boolean BLACK = true;

    /**
     * Node in the Tree.  Doubles as a means to pass key-value pairs back to
     * user (see Map.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;
        ……
    }

红黑树特点:

  • 根节点必为黑色
  • 叶节点可红可黑
  • 每条路径上黑节点数量(即黑深度)相等
  • 父子节点不能同时为红色
  • 最长路径的长度不超过最短路径长度的2倍即 h_{max}\leq h_{min}*2

相比于AVL树,红黑树插入时旋转次数基本一致,但是回溯步长为2,耗时更短;删除时旋转次数最多3次,AVL树最多O(logn)次,红黑树性能更好。但是红黑树一般更高(更不平衡)

TreeMap要求,要么节点的Key值实现了Comparable接口,要么传入一个合适的Comparator对象:

    final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }

并且优先使用Comparator进行比较

因此不需要重写Key的hashCode方法和equals方法。因为根本没用到这两个方法,像HashMap就会对key值进行比较:

    //HashMap类,putVal方法
    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;

如果两个要求都没有达成,则抛出ClasscastException

节点插入:put方法

先贴源码:

    public V put(K key, V value) {
        //先把root赋给当前节点,如果为空,代表是棵空树,则直接插入一个新节点
        Entry<K,V> t = root;
        if (t == null) {
            //用来检测Key是否实现了Comparable或TreeMap是否传入了Comparator
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            //不断地将传入的key值与当前节点的key值进行比较
            //如果传入的key值更大,则向右走,更小则向左,相等就直接进行值覆盖
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            //这里的过程和if分支的一样
            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);
        }
        //找到父节点以后,就构建节点,并且视情况称为左子结点或右子节点
        //直到此时还没开始进行红黑树的调整
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //fixAfterInsertion方法进行红黑树调整
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

可以看到,整个插入操作都没有涉及树的调整,直到插入完成才会进行

调整包括重新着色和左右旋转

    private void fixAfterInsertion(Entry<K,V> x) {
        //先把新插入节点设为红色,内部类Entry的默认值为BLACK
        x.color = RED;
        //循环条件:新节点不为空、新节点不是根节点、父节点是红色
        while (x != null && x != root && x.parent.color == RED) {
            //比较时不仅要在父子之间进行,还要在叔侄之间进行,因为要保证每条路径黑色节点数量一致
            //如果父节点是爷爷的左子结点,则对右叔节点进行考察
            if (parentOf(x) == leftOf(parentOf(parentOf(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 {
                    //如果叔叔是黑色,且自己是父节点的右子节点,就对父节点进行左旋
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    //把父节点染黑、祖父节点染红,对祖父节点进行右旋
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                //以下跟if分支类似
                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 {
                    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;
    }

红黑树插入操作中,如果发生了树的调整,存在以下三种情形:

  • 父节点和叔节点都是红色
  • 父节点是红色,叔节点是黑色
    • 自身是父节点左子结点,则右旋
    • 否则左旋

旋转代码如下(左旋):

    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;
        }
    }

左旋的过程就是右儿子当爹,自己变成左儿子,自己的孙节点变成右子节点;右旋反之

以书上的例子说明:按照 :插入 55 56 57 58 83 ,删除57 ,插入59 的顺序建树

图直接从书上拍照过来了:

55直接插入、染黑就行,56染红,57插入时,出现连续红节点,由于默认null节点是黑色,于是发生左旋

插入58时,又出现连续红色,此时父叔节点都是红色,则仅触发重新着色,不进行旋转,56从红变黑是因为根节点每次调整后都会染黑

插入83时再次需要调整,此时情况和57插入时类似,发生了左旋

57因为是叶节点,又是红色,因此可以直接删除

59插入时,再次进行调整,叔节点黑色,自己是左子结点,于是先右旋,右旋之后又触发了左旋条件,于是进行左旋

书上的说明到此就结束了

节点删除:实际调用deleteEntry方法

先贴源码:

    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // 如果p不是叶节点,就选一个继任者接替p,然后让p指向这个接任者
        if (p.left != null && p.right != null) {
            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) {
            // 让p的儿子认爹
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            //让p的爹认儿子
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;

            // 其他人都有儿子和爹了,p没有作用,因此可以孤立出来了(删除)
            p.left = p.right = p.parent = null;

            // 树结构调整
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { 
            // 如果p没儿子也没爹,说明它是唯一节点(根节点),那么只要把整个树置空即可
            root = null;
        } else { 
            //  如果p没儿子但是有爹
            //  那就先进行重新着色和旋转
            if (p.color == BLACK)
                fixAfterDeletion(p);
            // 如果调整完p还不是根节点,那么此时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;
            }
        }
    }

主要分三种情况:

  • p是叶节点:
    • p是黑色:先进行树调整,再删除
    • p是红色:直接删
  • p不是叶节点:先找p的爹或者儿子当遗产继承人,然后让p的儿子认p的爹当爹,让p的爹认p的儿子当儿子,然后把p删掉
    • p是黑色:进行树结构调整

successor是找继承人的方法:

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        //如果是空节点,那就没有可以继承的,直接退出
        if (t == null)
            return null;
        //如果有右儿子,让他的最左的一个叶节点继承
        else if (t.right != null) {
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            //没有右儿子就让爹来继承遗产
            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和插入的fixAfterInsertion方法类似,也是进行树结构调整的:

    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));
                    //取root,退出循环
                    x = root;
                }
            } else { 
                ……//作用差不多,省略了
            }
        }
        //最后确保root是黑色
        setColor(x, BLACK);
    }

这个调整相当绕人

还是用相同的例子,假设现在要删掉59:

根据代码,会找83代替59,之后只要把多余的83叶节点删除即可,由于该节点是红色,删除也不会违反红黑树特性,因此可以直接删除

猜你喜欢

转载自blog.csdn.net/u010670411/article/details/85929628
今日推荐