Java集合源码解析:TreeMap

本文概要

  1. 二叉查找树的用处
  2. 二叉查找树,以及二叉树带来的问题
  3. 平衡二叉树的好处
  4. 红黑树的定义以及构造
  5. 红黑树在TreeMap的运用

二叉树的好处

可能许多人会有疑问,为什么要使用二叉树,有那么多的数据结构,比如数组、链表等

简单看下数组和链表的优缺点

数组

  • 优势:查找快,通过索引直接定位数据。时间复杂度O(1)
  • 劣势:删除和插入元素比较麻烦,需要移动的元素比较多。时间复杂度O(n)

链表

  • 优势:删除和插入比较方便,直接修改指针,时间复杂度O(1)
  • 劣势:查找慢,需要沿着头指针挨个去对比,时间复杂度O(n)

那么二叉树则是结合了上面两种数据结构的优势,并且它是有序的,而且在处理大批量的动态数据是比较有用的。它的时间复杂度O(logN)

二叉查找树

先来看看二叉查找树的定义:

  1. 要么是一颗空树,要么就是一颗具有如下特性的二叉树
  2. 左节点的值必须小于等于父节点的值
  3. 右节点的值必须大于等于父节点的值

每个节点都符合这个特性,所以它是有序的,也便于查找,如下图:

Java集合源码解析:TreeMap_第1张图片

但是在一种极端的情况下,二叉查找树会出现不平衡。如果一棵二叉树,只有左子树或者右子树,就变成了一个链表,查找的效率就变的很慢,如下图:
Java集合源码解析:TreeMap_第2张图片

对于查找而言,二叉查找树的查找是跟树的高度是有关系的,如果一棵树的高度为N,那么最多可以在N步内完成查找,所以树的高度越矮,那么查询的效率就越高。考虑到一般情况,左子树和右子树的高度不能相差太大,所以我们都希望二叉查找树两边子树是平衡的,而不是只有一边子树。为了优化因左右子树高度不稳定对查找效率的影响,于是出现了平衡二叉树

平衡二叉树

先看平衡二叉树的定义:

  1. 它是一颗二叉树
  2. 它的左子树和右子树的深度差的绝对值不超过1

Java集合源码解析:TreeMap_第3张图片

在构造平衡二叉树时,新增一个节点,可能会造成二叉树的失衡,失衡调整主要是通过旋转最小失衡树来实现。

失衡调整主要分为4种情况:

  • LL型
    Java集合源码解析:TreeMap_第4张图片

当插入“7”节点,是最小失衡树的左子树的“8”左节点。很显然,是“9”的左子树过高,那么以"9"节点为轴心右旋

  • LR型
    Java集合源码解析:TreeMap_第5张图片

当插入"8"节点,是最小失衡树左子树的“7”的右节点。首先以“7”为轴心,然后左旋,变成了LL型,然后以“9”为轴心右旋。

  • RR型
    Java集合源码解析:TreeMap_第6张图片
    当插入“11”节点,是最小失衡树的右子树的“10”右节点。很显然,是“9”的右子树过高,那么以"9"节点为轴心左旋

  • RL型
    Java集合源码解析:TreeMap_第7张图片

当插入"11"节点,是最小失衡树右子树的“12”的左节点。首先以“12”为轴心,右旋,变成了RR型,然后以“10”为轴心右旋。

红黑树

先来看看红黑树的定义

  1. 每个节点要么红色,要么黑色
  2. 根节点是黑色
  3. 所有叶子节点是黑色,即空节点(NIL)
  4. 每个红色节点的两个子节点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从一个节点到其所有叶子节点的所有路径上包含相同数目的黑节点

注意:

  • 特性3中的叶子节点,是只为空(NIL或null)的节点。
  • 特性5,确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。因此在最坏情况下,红黑树能保证时间复杂度为O( lgn )

Java集合源码解析:TreeMap_第8张图片

当对红黑树进行插入和删除时,可能会破坏红黑树的性质,那么就需要通过修改某些节点颜色树的旋转来恢复红黑树的性质

树的旋转,分为左旋和右旋,如下图

  • 左旋
    Java集合源码解析:TreeMap_第9张图片
    对A节点进行左旋,首先找到A节点的右孩子节点B,让B节点的左孩子节点C指向A节点的右孩子节点,再把A节点指向B节点的左孩子节点。

  • 右旋
    Java集合源码解析:TreeMap_第10张图片

对A节点进行右旋,首先找到A节点的左孩子节点B,让B节点的右孩子节点D执行A节点的右孩子节点,在把A节点执行B节点的右孩子节点。

红黑树的插入

向一棵含有n个节点的红黑树插入一个新节点的操作可以在O(lgn)时间内完成。
在继续插入操作分析前,再来复习下红黑树的特性:

  1. 每个节点要么红色,要么黑色
  2. 根节点是黑色
  3. 所有叶子节点是黑色,即空节点(NIL)
  4. 每个红色节点的两个子节点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从一个节点到其所有叶子节点的所有路径上包含相同数目的黑节点

规则:

  • 在红黑树插入节点时,节点的初始颜色是红色,这样可以尽量避免对树的结构进行调整(参考第5个规则)
  • 但是插入红色节点的时候,不会破坏第5个规则,但是可能会破坏第4个规则,所以这时候就需要通过修改某些节点的颜色、对某些节点进行旋转,来维持红黑树的性质
  • 在删除节点是时候,如果删除的节点为黑色,可能会破坏第5个规则,那么同样需要修复树的结构,以进行维护树的性质

插入节点可以分为7种情况进行处理

  1. 空树中插入节点
  2. 插入节点的父节点是黑色
  3. 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是左节点,父节点是左节点
  4. 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是左节点,父节点是右节点
  5. 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是右节点,父节点是右节点
  6. 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是右节点,父节点是左节点
  7. 插入节点的父节点是红色,且叔叔节点也是红色

情况一:空树中插入节点

违反:性质2

修复策略:把插入节点修改为黑色即可

情况二:插入节点的父节点是黑色

违反:未违反任何性质

修复策略:什么都不做

情况三: 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是左节点,父节点是左节点

违反:性质4

修复策略:

  1. 把父节点颜色修改为黑色
  2. 把祖父节点颜色修改为红色
  3. 对祖父节点进行右旋

如下图所示:
Java集合源码解析:TreeMap_第11张图片

情况四: 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是左节点,父节点是右节点

违反:性质4

修复策略:

  1. 对父节点进行右旋
  2. 把自己节点颜色修改为黑色,祖父节点修改为红色
  3. 对祖父节点进行左旋

如下图:
Java集合源码解析:TreeMap_第12张图片

情况五: 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是右节点,父节点是右节点

违反:性质4

修复策略:

  1. 把父节点颜色修改为黑色
  2. 把祖父节点颜色修改为红色
  3. 对祖父节点进行左旋

图就不画,跟情况三类型

情况六: 插入节点的父节点是红色,且叔叔节点是黑色,当前节点是右节点,父节点是左节点

违反:性质4

修复策略:

  1. 对父节点进行左旋
  2. 把自己节点修改为黑色,把祖父节点颜色修改为红色
  3. 对祖父节点进行右旋

图就不画,跟情况四类型

情况七: 插入节点的父节点是红色,且叔叔节点是红色

违反:性质4

修复策略:

  1. 把父节点颜色和叔叔节点颜色修改为黑色
  2. 把祖父节点颜色修改为红色
  3. 然后会变成情况一至七的情况,继续按情况进行分析

删除节点对节点的调整,我们在TreeMap在进行分析

TreeMap

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的判定通过其本身自带的自然排序,也可以通过构造器传入Comparator比较器。

TreeMap底层是通过红黑树实现,也就意味着containsKey(),get(),put(),remove()的时间复杂度都为O(log(n))

首先来看看TreeMap构造器

    public TreeMap() {
        comparator = null;
    }
    
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

	public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }
	
	public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }
    

TreeMap的成员变量

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    // key的比较器
    private final Comparator<? super K> comparator;

	// 树的根节点
    private transient Entry<K,V> root;

     // 树的节点个数
    private transient int size = 0;

     // 对树的修改次数
    private transient int modCount = 0;
    
    ````省略代码
}

下面我们依次来看get()、put()、remove()方法

get()

    public V get(Object key) {
    	// get方法实际上调用的是getEntry()
        Entry<K,V> p = getEntry(key);
        // 如果p节点存在,则返回p节点的value,否则返回null
        return (p==null ? null : p.value);
    }
    
    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        // 如果创建TreeMap的时候传入了比较器,那么调用getEntryUsingComparator(key)        
		// getEntryUsingComparator(key)跟getEntry(key)逻辑差不多,只不过一个使用了自定义比较器去比较key,一个使用自身的比较器去比较key		       
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        // 把key强转为比较器
        Comparable<? super K> k = (Comparable<? super K>) key;
        // 获取根节点
        Entry<K,V> p = root;
        while (p != null) {
       		// key与根节点的key进行比较
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
            	// key小,则把左节点赋给p进行循环
                p = p.left;
            else if (cmp > 0)
            	// key大,则把右节点赋给p进行循环
                p = p.right;
            else
            	// 相等,直接返回p节点
                return p;
        }
        return null;
    }

get()方法还是比较简单的,从根节点开始,依次对节点的key进行判断,如果大于节点的key则继续判断节点的右孩子节点,以此类推,直到找到相等key的节点。上面都有注释讲的非常清楚了。

put()

    public V put(K key, V value) {
        Entry<K,V> t = root;
        // 如果根节点为空
        if (t == null) {
            compare(key, key); // type (and possibly null) check
			// 直接创建一个Entry,赋给根节点
            root = new Entry<>(key, value, null);
            // 树节点的大小赋值为1
            size = 1;
            // 修改次数+1
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        // 获取比较器
        Comparator<? super K> cpr = comparator;
        // 判断该key是否存在,如果存在直接找到该节点,把节点的值修改为新value,然后直接返回
        if (cpr != null) {
            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;
            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不存在
        // 创建一个新的Entry节点
        Entry<K,V> e = new Entry<>(key, value, parent);
        // key与parent的key进行比较
        if (cmp < 0)
        	// key小,把新节点指向parent的左节点
            parent.left = e;
        else
        	// key大,把新节点指向parent的右节点
            parent.right = e;
        // 添加了一个红色的新节点,可能会破坏原来的红黑树结构,那么需要进行修复
        fixAfterInsertion(e);
        // 节点+1
        size++;
        // 修改次数+1
        modCount++;
        return 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)))) {
                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 {
                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;
    }

put()方法总结:

  • 首先获取树根节点,如果根节点为空,那么直接创建一个新节点指向根节点
  • 根节点不为空,然后判断树里面是否存在节点的key与添加的key相等
  • 如果相等,那么直接把该节点的value替换成新的value
  • 如果不相等,然后创建一个新的节点,添加到红黑树中
  • 添加了一个新的节点可能会破坏树的结构,那么调用fixAfterInsertion(),进行红黑树结构进行调整。

fixAfterInsertion()跟我们在上面讲红黑树插入的情况,已经讲的非常清楚了。

remove()

    public V remove(Object key) {
    	// 首先判断该key是否存在
        Entry<K,V> p = getEntry(key);
        if (p == null)
        	// 不存在直接返回null
            return null;

        V oldValue = p.value;
        // 调用deleteEntry()删除节点
        deleteEntry(p);
        return oldValue;
    }
    
    private void deleteEntry(Entry<K,V> p) {
    	// 修改次数+1
        modCount++;
        // 树节点个数-1
        size--;
		
		// 如果p节点的左右孩子节点都不为空,那么调用successor(p)寻找后继节点
        if (p.left != null && p.right != null) {
        	// 寻找后继节点逻辑很简单
        	// 即为:p节点的右子树的最小的那个元素,即为p的后继节点
            Entry<K,V> s = successor(p);
            // 把p替换成后继节点
            p.key = s.key;
            p.value = s.value;
            p = s;
        }        
        // 获取p节点的左孩子节点,如果左孩子节点不存在,则获取p节点的右孩子节点
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
			
		// 如果p节点的孩子节点不为空
        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;

            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { 
        	// 如果p的父节点为null,则为root节点
            root = null;
        } else { 
        	// 如果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;
            }
        }
    }

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;
            // 沿着t的右节点的左子树找到最小的元素
            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(),对删除节点的树,进行修复

    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);							情况1
                    setColor(parentOf(x), RED);						情况1
                    rotateLeft(parentOf(x));						情况1
                    sib = rightOf(parentOf(x));						情况1
                }

                if (colorOf(leftOf(sib))  == BLACK &&				情况2
                    colorOf(rightOf(sib)) == BLACK) {				情况2
                    setColor(sib, RED);								情况2
                    x = parentOf(x);								情况2
                } else {
                    if (colorOf(rightOf(sib)) == BLACK) {			情况3
                        setColor(leftOf(sib), BLACK);				情况3
                        setColor(sib, RED);							情况3
                        rotateRight(sib);							情况3
                        sib = rightOf(parentOf(x));					情况3
                    }
                    setColor(sib, colorOf(parentOf(x)));			情况4
                    setColor(parentOf(x), BLACK);					情况4
                    setColor(rightOf(sib), BLACK);					情况4
                    rotateLeft(parentOf(x));						情况4
                    x = root;										情况4
                }
            } else { // symmetric
                Entry<K,V> sib = leftOf(parentOf(x));

                if (colorOf(sib) == RED) {							情况5
                    setColor(sib, BLACK);							情况5
                    setColor(parentOf(x), RED);						情况5
                    rotateRight(parentOf(x));						情况5
                    sib = leftOf(parentOf(x));						情况5
                }

                if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {				情况6
                    setColor(sib, RED);								情况6
                    x = parentOf(x);								情况6
                } else {
                    if (colorOf(leftOf(sib)) == BLACK) {			情况7
                        setColor(rightOf(sib), BLACK);				情况7	
                        setColor(sib, RED);							情况7
                        rotateLeft(sib);							情况7
                        sib = leftOf(parentOf(x));					情况7	
                    }
                    setColor(sib, colorOf(parentOf(x)));			情况8
                    setColor(parentOf(x), BLACK);					情况8
                    setColor(leftOf(sib), BLACK);					情况8
                    rotateRight(parentOf(x));						情况8
                    x = root;										情况8
                }
            }
        }

        setColor(x, BLACK);
    }

下面以remove(12)进行2个图解解释fixAfterDeletion()
Java集合源码解析:TreeMap_第13张图片

Java集合源码解析:TreeMap_第14张图片

fixAfterDeletion()的中心思想:将情况1首先转换为情况2或者情况3和情况4,当前调整情况并不一定从情况1开始。情况5~情况8跟前四种情况是对称的,就没有画出这四种情况了,可以参考代码自行理解。

到此为止TreeMap已经分析完了,其实大多数时间都在讲红黑树的着色、旋转、修复,因为TreeMap底层就是红黑树。在数据结构中红黑树是比较难懂的,其算法也比较复杂,所以对于树的理解一定要多看图画图。

为什么TreeMap底层不使用平衡二叉树而使用红黑树

那是因为平衡二叉是高度平衡的树, 而每一次对树的修改, 都要 rebalance, 这里的开销会比红黑树大。 如果插入一个node引起了树的不平衡,平衡二叉树和红黑树都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,平衡二叉树需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而根据fixAfterDeletion(),我们可知红黑树最多只需3次旋转,只需要O(1)的复杂度, 所以平衡二叉树需要rebalance的频率会更高,因此红黑树在大量插入和删除的场景下效率更高

你可能感兴趣的:(Java集合源码解析,Java集合源码解析,Java集合类,Java,TreeMap,红黑树)