TreeMap源码分析 红黑树
今天尝试刨一下TreeMap的祖坟。
底层结构对比
先来看一下与HashMap、LinkedHashMap和TreeMap的对比,同时就当是复习一下:
- HashMap使用数组存储数据,并使用单向链表结构存储hash冲突数据,同一个冲突桶中数据量大的时候(默认超过8)则使用红黑树存储冲突数据。
- LinkedHashMap使用数组+双向链表存储数据,冲突数据存储方式同HashMap。
- TreeMap使用红黑树存储数据,注意是直接使用红黑树,不使用table数组。
关于排序特性
- HashMap无顺序,不能保持顺序。
- LinkedHashMap能保持写入的顺序,遍历的时候可以按照写入顺序获取数据。
- TreeMap是有序的Map,自动按照key值排序存储,遍历时获取到的是有序数据。
需要注意LinkedHashMap和TreeMap在顺序方面的区别,LinkedHashMap只能保持写入顺序,从“排序”的角度讲,他实际是无序的。
只有TreeMap是可以实现自动排序的。
TreeMap按照什么排序?
TreeMap底层支持两种排序方式:
- TreeMap对象实例化时传入comparator对象。
- key值对象实现Comparable接口。
如果以上两点都不能满足的话,向TreeMap对象put数据的时候会抛出运行时异常。
比如TreeMap<String,Object>,由于String实现了Comparable接口,所以是没有问题的。
但是如果自定义的对象,没有实现Comparable接口,同时在TreeMap实例化的时候没有设置comparator对象,则该TreeMap对象实际是不可用的。
TreeMap是否可以存储null?
指的是,是否可以存储key为空的数据?我们知道HashMap是可以支持唯一一个null对象的。
很多人都说不可以,但是我觉得有条件可以,但是没做测试(因为感觉则个问题其实有点扯)。
条件是实例化TreeMap对象的时候指定comparator对象,同时,该comparator对象的compare方法可以支持null。
研究TreeMap的put源码,也可以发现对以上说法的支持:
Comparator<? super K> cpr = comparator;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;elsereturn t.setValue(value);} while (t != null);}else {if (key == null)throw new NullPointerException();...省略若干代码
可以发现如果有comparator的话,put方法不会立即抛出异常。但是如果comparator对象的compare方法不能支持null的话,一样会抛出异常。
put方法
由于TreeMap支持自动排序,所以put方法会检查是否满足规则。
不满足排序规则,抛出异常。
否则,按照红黑树算法规则要求,创建红黑树,存储数据。
所以这里就涉及到一个重要的数据结构:红黑树。
二叉树BST & 平衡二叉树AVL
红黑树是树结构的一种,是比二叉树和平衡二叉树更加复杂的一种数据结构。所以我们先从简单的入手,了解一下二叉树。
树结构其实是实现了子排序的一种数据结构,我们说到的树结构一般指的是二叉树、也叫二叉搜索树(BST - Binary Search Tree),定义:它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
二叉搜索树的插入、搜索操作所花的时间都和树的高度成正比。因此,如果共有n个元素,那么平均每次操作需要O(logn)的时间。
二叉搜索树的特性并不能保证他是平衡的,也就是说,极端情况下,一颗二叉搜索树从根节点开始,只有左节点、没有右节点(比如一直按照从大到小或者相反顺序插入二叉树),这种情况下,二叉树就蜕变成了链表,查询时间复杂度就会下降为O(n)。
改善二叉树这一缺点的经典数据结构是平衡二叉树AVL(Adelson-Velsky and Landis Tree,以两位发明者命名)。平衡二叉树的特性:
1.本身首先是一棵二叉搜索树。
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
一颗平衡二叉树在新节点插入之后可能会失衡,包括以下四种场景:
左左失衡LL
插入的数据在左侧不平衡树的左侧,这种情况下需要进行右旋操作再次平衡:
我们需要记住一个概念:平衡二叉树失衡之后通过旋转动作使得它再次平衡的处理,只是针对失衡的子树、不需要对整个树进行操作。比如上图新的节点1加入之后,导致pivot节点7一侧的树失衡,我们需要处理的是包含pivot节点的root节点的左侧树这一部分子树,右侧节点18下即使有再多的节点,也不需要处理。
右旋操作的实质是:以pivot节点7为支点做顺时针旋转,旋转之后7变为自己原来父节点的父节点、7的左节点不变,7的右节点变为7原来的父节点的左节点。
右右失衡RR
插入的节点在右侧不平衡树的右侧,这种情况下需要左旋:
左旋操作的实质是:以pivot节点18为支点做逆时针旋转,旋转之后18变为自己原来父节点的父节点、18的右节点不变,18的左节点变为18原来的父节点的右节点。
左右失衡LR
插入的新节点在左侧不平衡树的右侧。先左旋再右旋
右左失衡LR
插入的节点在右侧不平衡树的左侧,先右旋再左旋:
平衡二叉树具有:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1 这一特性,这一严格条件会导致每次插入新节点之后总会大概率破坏平衡、从而必须要通过上述的旋转操作使其再次达到平衡,旋转操作会影响数据插入的效率。
红黑树可以解决这一问题。
红黑树
红黑树是一种带颜色(节点是红色或者黑色)的平衡二叉树,具有以下特性:
性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
红黑树新加入节点的颜色默认为黑色,因为根据红黑树性质4,每个红色节点的两个子节点都是黑色。所以,新加入节点如果是黑色的话,可以检查其父节点如果是红色的话,可以自动满足性质4从而减少再平衡操作、提高数据加入的效率。这一点可以通过TreeMap的put方法源码得到验证:
public V put(K key, V value) {Entry<K,V> t = root;if (t == null) {compare(key, key); // type (and possibly null) checkroot = new Entry<>(key, value, null);size = 1;modCount++;return null;}int cmp;Entry<K,V> parent;// split comparator and comparable pathsComparator<? super K> cpr = comparator;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;elsereturn 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;elsereturn t.setValue(value);} while (t != null);}Entry<K,V> e = new Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;fixAfterInsertion(e);size++;modCount++;return null;}
put方法前半部分逻辑比较简单,为新节点找到合适的位置加入,之后会调用fixAfterInsertion检查是否破坏了红黑树的规则从而需要执行再平衡操作:
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;}
fixAfterInsertion方法首先判断其父节点是红色的话,则不做任何操作。
get方法
根据红黑树查找算法查找并返回数据,红黑树是平衡二叉树,查询时间复杂度为O(log(n))。
key遍历
比如调用TreeMap.keySet方法,采用遍历二叉树算法,按照从小到大的顺序返回所有key值组成的循环器。
我该使用哪一个?
需要用到Map的时候,到底该使用哪一个的问题:
- 我只需要一个存储数据的容器,没有具体要求的话,用HashMap。
- 存储数据后,有按照存储顺序获取数据的需求,采用LinkedHashMap。
- 希望存储数据的同时,帮助实现自动排序,采用TreeMap。
性能的问题,其实几乎不需要考虑,不过我们还是需要知道:
- HashMap和LinkedHashMap查询速度快,理想情况下时间复杂度几乎是O(1)。
- HashMap写入速度最快,LinkedHashMap写入速度与HashMap几乎相同,TreeMap写入速度最慢(理论上,实际数据量小的情况下未必慢)。
- 遍历速度相差无几,理论上HashMap会慢一点,因为需要遍历空桶。
并发问题尚待研究,但是我们清楚地知道,以上三种均不具备线程安全性。
好梦!