《Java集合Map》TreeMap

1. 实现原理

TreeMap底层是基于平衡二叉树实现,也称为红黑树。它首先是一棵二叉树,具有二叉树所有的特性,即树中的任何节点的值大于它的左子节点,且小于它的右子节点,如果是一棵左右完全均衡的二叉树,元素的查找效率将获得极大提高。最坏的情况就是一边倒,只有左子树或只有右子树,这样势必会导致二叉树的检索效率大大降低。红黑树,就是节点是红色或者黑色的平衡二叉树,它维持了二叉树的平衡,因此效率很高,其时间复杂度为O(log n)。TreeMap最重要的特点就是可排序

二叉树的查找流程:先将目标值和根节点的值进行比较,如果目标值小于根节点的值,则再和根节点的左孩子进行比较。如果目标值大于根节点的值,则继续和根节点的右孩子比较。在查找过程中,如果目标值和二叉树中的某个节点值相等,则返回true,否则返回false。

红黑树是一个更高效的平衡二叉树,有以下特性:

  • 节点是红色或黑色;
  • 根节点是黑色
  • 叶子节点都是黑色的空节点(NIL节点);
  • 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
  • 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
  • 新加入的节点为红色节点
《Java集合Map》TreeMap_第1张图片

从根节点到叶子节点的最长路径不大于最短路径的2倍

因为插入、删除和查找操作的最坏情况执行时间都要求与树的高度成比例,即使是在最坏的情况下执行效率都是高效的,其检索效率O(log n)。

最短路径:由于从根节点到每个叶子节点的黑色节点数量是一样的,那么纯由黑色节点组成的路径就是最短路径。
最长路径:若有红色节点,则必然有一个连接的黑色节点,当红色节点和黑色节点数量相同时,就是最长路径,也就是黑色节点(或红色节点)*

新加入到红黑树中的节点为红色节点

由于红黑树从根节点到每个叶子节点的黑色节点的数量是一样的,如果新加入的节点是黑色节点的话,必然破坏规则。但是加入红色节点却不一定,除非其父节点就是红色节点。因此加入红色节点,破坏规则的可能性小一些。

红黑树平衡被破坏后,内部通过旋转和变色两种方式保持结平衡;旋转分为:左旋和右旋。

2. put()

《Java集合Map》TreeMap_第2张图片
public V put(K key, V value) {
    Entry t = root;
    /**
     * 如果根节点都为null,还没建立起来红黑树,我们先new Entry并赋值给root把红黑树建立起来,这个时候红
     * 黑树中已经有一个节点了,同时修改操作+1。
     */
    if (t == null) {
        compare(key, key); 
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    /**
     * 如果节点不为null,定义一个cmp,这个变量用来进行二分查找时的比较;定义parent,是new Entry时必须
     * 要的参数
     */
    int cmp;
    Entry parent;
    // cpr表示有无自己定义的排序规则,分两种情况遍历执行
    Comparator cpr = comparator;
    if (cpr != null) {
        /**
         * 从root节点开始遍历,通过二分查找逐步向下找
         * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法
         * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的keyroot.key,
         * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key,
         * 那么直接根据root节点的value值即可。
         * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
         *
         * 需要注意的是:这里并没有对key是否为null进行判断,建议自己的实现Comparator时应该要考虑在内
         */
        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 {
        // 从这里看出,当默认排序时,key值是不能为null的
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable k = (Comparable) 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);
    }
    /**
     * 能执行到这里,说明前面并没有找到相同的key,节点已经遍历到最后了,我们只需要new一个Entry放到
     * parent下面即可,但放到左子节点上还是右子节点上,就需要按照红黑树的规则来。
     */
    Entry e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    /**
     * 节点加进去了,并不算完,我们在前面红黑树原理章节提到过,一般情况下加入节点都会对红黑树的结构造成
     * 破坏,我们需要通过一些操作来进行自动平衡处置,如【变色】【左旋】【右旋】
     */
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
无需调整 【变色】即可实现平衡 【旋转 + 变色】才可实现平衡
情况 1: 当父节点为黑色时插入子节点 空树插入根节点,将根节点红色变为黑色 父节点为红色左节点,叔父节点为黑色,插入左子节点,那么通过【左左节点旋转】
情况 2: - 父节点和叔父节点都为红色 父节点为红色左节点,叔父节点为黑色,插入右子节点,那么通过【左右节点旋转】
情况 3: - - 父节点为红色右节点,叔父节点为黑色,插入左子节点,那么通过【右左节点旋转】
情况 4: - - 父节点为红色右节点,叔父节点为黑色,插入右子节点,那么通过【右右节点旋转】
private void fixAfterInsertion(Entry x) {
    // 新插入的节点为红色节点
    x.color = RED;
    // 父节点为黑色时,并不需要进行树结构调整,只有当父节点为红色时,才需要调整
    while (x != null && x != root && x.parent.color == RED) {
        // 如果父节点是左节点,对应上表中情况1和情况2
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry 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 {
            // 父节点是右节点的情况
            Entry 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;
}

源码中通过rotateLeft进行左旋,通过rotateRight进行右旋。都非常类似,我们就看一下左旋的代码,左旋规则如下:“逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点”。

《Java集合Map》TreeMap_第3张图片
image

3. get()

get方法是通过二分查找的思想,我们看一下源码:

public V get(Object key) {
    Entry p = getEntry(key);
    return (p==null ? null : p.value);
}
/**
 * 从root节点开始遍历,通过二分查找逐步向下找
 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过k.compareTo(p.key)比较传入的key和
 * 根节点的key值;
 * 如果传入的keyroot.key, 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;
 * 如果恰好key==root.key,那么直接根据root节点的value值即可。
 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
 */
// 默认排序情况下的查找
final Entry getEntry(Object key) {
    
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable k = (Comparable) key;
    Entry p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}
/**
 * 从root节点开始遍历,通过二分查找逐步向下找
 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法
 * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的keyroot.key,
 * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key,
 * 那么直接根据root节点的value值即可。
 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
 */
// 自定义排序规则下的查找
final Entry getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
    K k = (K) key;
    Comparator cpr = comparator;
    if (cpr != null) {
        Entry p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

4. remove()

remove方法可以分为两个步骤,先是找到这个节点,直接调用了上面介绍的getEntry(Object key),这个步骤我们就不说了,直接说第二个步骤,找到后的删除操作。

public V remove(Object key) {
    Entry p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}
  • 删除的是根节点,则直接将根节点置为null;
  • 待删除节点的左右子节点都为null,删除时将该节点置为null;
  • 待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可;
  • 待删除节点的左右子节点都不为null,则找前驱或者后继,将前驱或者后继的值复制到该节点中,然后删除前驱或者后继(前驱:左子树中值最大的节点,后继:右子树中值最小的节点)。
private void deleteEntry(Entry p) {
    modCount++;
    size--;
    //当左右子节点都不为null时,通过successor(p)遍历红黑树找到前驱或者后继
    if (p.left != null && p.right != null) {
        Entry s = successor(p);
        //将前驱或者后继的key和value复制到当前节点p中,然后删除节点s(通过将节点p引用指向s)
        p.key = s.key;
        p.value = s.value;
        p = s;
    } 
    Entry replacement = (p.left != null ? p.left : p.right);
    /**
     * 至少有一个子节点不为null,直接用这个有值的节点替换掉当前节点,给replacement的parent属性赋值,给
     * parent节点的left属性和right属性赋值,同时要记住叶子节点必须为null,然后用fixAfterDeletion方法
     * 进行自平衡处理
     */
    if (replacement != null) {
        //将待删除节点的子节点挂到待删除节点的父节点上。
        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.left = p.right = p.parent = null;
        /**
         * p如果是红色节点的话,那么其子节点replacement必然为红色的,并不影响红黑树的结构
         * 但如果p为黑色节点的话,那么其父节点以及子节点都可能是红色的,那么很明显可能会存在红色相连的情
         * 况,因此需要进行自平衡的调整
         */
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {//这种情况就不用多说了吧
        root = null;
    } else { 
        /**
         * 如果p节点为黑色,那么p节点删除后,就可能违背每个节点到其叶子节点路径上黑色节点数量一致的规则,
         * 因此需要进行自平衡的调整
         */ 
        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;
        }
    }
}

删除之后会触发自平衡操作fixAfterDeletion

private void fixAfterDeletion(Entry x) {
    /**
     * 当x不是root节点且颜色为黑色时
     */
    while (x != root && colorOf(x) == BLACK) {
        /**
         * 首先分为两种情况,当前节点x是左节点或者当前节点x是右节点,这两种情况下面都是四种场景,这里通过
         * 代码分析一下x为左节点的情况,右节点可参考左节点理解,因为它们非常类似
         */
        if (x == leftOf(parentOf(x))) {
            Entry sib = rightOf(parentOf(x));

            /**
             * 场景1:当x是左黑色节点,兄弟节点sib是红色节点
             * 兄弟节点由红转黑,父节点由黑转红,按父节点左旋,
             * 左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点
             */
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            /**
             * 场景2:节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变
             * 红,同时将x指向当前x的父节点
             */
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                /**
                 * 场景3:节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时,
                 * 需要将sib左子节点设置为黑色,sib节点设置为红色,同时按sib右旋,再将sib指向x的
                 * 兄弟节点
                 */
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                /**
                 * 场景4:节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、
                 * 左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色,
                 * 设置x的父节点为黑色,设置sib右子节点为黑色,左旋x的父节点p,然后将x赋值为root
                 */
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else {//x是右节点的情况
            Entry 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);
}

你可能感兴趣的:(《Java集合Map》TreeMap)