这篇文章基本来自《码出高效》这本书, 由我自己总结归纳一些基础性的知识。
部分图和源代码来自于CarpenterLee博客

从最简单的树说起

1.树(Tree)

相对来说,树是一个很基础的概念, 不需要去多谈。
需要掌握两个概念:
深度:从根节点出发,到某节点边的条数。
高度:从某结点出发,到叶子节点为止, 最长简单路径上边的条数。

2.平衡二叉树

高度差为 1 的二叉树。
其性质如下:
(1)树的左右高度差不能超过 1
(2)任何往下递归的左子树和右子树, 必须符合第一条性质。
(3)没有任何节点的空树或只有根节点的树也是平衡二叉树。

3.二叉查找树(又名二叉搜索树,Binary Search Tree)

性质:对于任意节点,它的左子树上所有节点的值都小于他, 而他的右子树上的所有节点的值都大于他。
遍历节点的三种方式:前序遍历、中序遍历、后序遍历。**他们三者规律如下**
(1)在任何递归子树中, 左节点一定在右节点之前遍历。
(2)前序、中序、后序,仅指根节点在遍历时的位置顺序。

AVL 树与树形旋转

AVL是一种平衡二叉查找树, 增加或删除节点后通过树形旋转重新达到平衡。

右旋:以某个节点为中心, 将他沉入当前子节点的位置, 而让他当前的子节点作为新树的根节点,也称为顺时针旋转;
左旋:以某个节点为中心, 将他沉入当前子节点的位置, 而让他当前的子节点作为新树的根节点,也称为逆时针旋转;

左旋图示
左旋图示
右旋图示
右旋图示

左旋源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Rotate Right
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null) l.right.parent = p;
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;
}
}

红黑树

红黑树和 AVL 树类似,但不准求所有的递归子树高度差不超过 1,而是保证**从根节点到叶尾的最长路径不超过最短路径的两倍,所以他的最坏运行时间也是$O(\text{log}n)$
它的五个约束条件:
1.节点只能是红色或者黑色
2.根节点必须是黑色
3.所有 NIL$^1$ 节点都是黑色
4.一条路径上不能出现相邻的两个红色节点
5.在任何递归子树内,根节点到叶子节点的所有路径上包含相同数目的黑色节点

【说明1】NIL 指在叶子节点不存在的两个虚拟节点,默认是黑色的。

红黑树 VS AVL树

再插入时, 红黑树和AVL树都能在至多两次旋转内恢复平衡。在删除时由于红黑树只追求大致上的平衡, 因此红黑树能在至多三次旋转内恢复和平;而 AVL 树追求绝对平衡, 至多旋转$O(\text{log}n)$次。

因此,面对频繁的插入和删除,红黑树较为合适;面对低频修改、大量查询时,AVL相对合适。

TreeMap

TreeMap 是按照 Key 的排序结果来组织内部结构的 Map 类集合, 它改变了 Map 类散乱无序的形象。 虽然 TreeMap 没有 ConcurrentHashMap 和 HashMap 普及(毕竟插入和删除的效率远没有后两者高),但是在 Key 有排序要求的场景下, 使用 TreeMap 可以事半功倍。

在 TrreMap 的接口继承树, 有两个接口:SortedMap 和 NavigableMap。SortedMap 接口表示它的 Key 是有序不可重复的, 支持获取头尾 Key-Value 元素, 或者根据 Key 指定范围获取子集合。插入的 Key 必须实现 Comparable 或提供额外的比较器 Comparator ,所以 Key 不能是 null ,但是 Value 可以;
NavigableMap 接口继承了 SortedMap ,根据指定的搜索条件返回最匹配的 Key-Value 元素。 不同于 HashMap ,TreeMap 并非一定要覆写 hashCode()equals() 方法来达到Key去重的目的。

关于 TreeMap 和 HashMap 的例子请看《码处高效》P191-P192 ,这里不多做赘述。

类名与属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TreeMap<K,V> extends AbstractMap<K,V> 
implements NavigableMap<K,V>, Cloneable,java.io.Serializable{
//排序时用的比较器,put源码解析时会用到
private final Comparator<? super K> Comparator;
//根节点,put源码时会提到
private transient Entry<K,V> root;
//定义成为字面含义的常量。下方fixAfterInsertion()解析时会用到
private static final boolean RED = false;
private static final boolean BLACK = true;

//TreeMap 的内部类, 存储红黑树节点的载体类 ,在整个TreeMap 中高频出现
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; //指向父节点的引用
//节点颜色信息是红黑树的精髓所在,默认是黑色
}
}

TreeMap 通过 put()deleteEntry() 实现红黑树的增加和删除节点操作,下面的源码分析以插入主流程为例。
再插入新节点之前,要明确三个前提条件:

  1. 需要调整的新节点总是红色的。
  2. 如果插入新节点的父节点是黑的,无需调整,因为依然能符合红黑树的5个约束条件。
  3. 如果插入新节点的父节点是红色的,因为红黑树规定不能出现相邻的两个红色节点,所以进入循环判断,或重新着色,或左右旋转,最终达到红黑树的五个约束条件,退出条件如下:
1
while(x != null && x != root && x.parent.color == RED)

put 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public V put(K key, V value) {
// t 表示当前节点,记住这个很重要! 先把 TreeMap 的根节点root引用赋值给当前节点
Entry<K,V> t = root;
// 如果当前节点为 null ,即是空树,新增的 KV 形成的节点就是根节点
if (t == null) {
//看似多此一举, 实际上预检了Key是否可以比较
compare(key, key);

//使用 KV 构造出的新的 Entry 对象, 其中第三个参数是 parent,根节点没有父节点
root = new Entry<K,V>(key, value, null);
size = 1;
modCount++;
return null;
}
// 记录比较结果
int cmp;
Entry<K,V> parent;
// 构造方法中置入的外部比较器
Comparator<? super K> cpr = comparator ;
// 重要步骤:根据二叉树的性质, 找到新节点插入合适的位置
if (cpr != null) {
// 循环的目标:根据参数 Key 与当前节点的 Key 不断进行对比
do {
// 当前节点赋值给父节点, 故从根节点开始遍历比较
parent = t;
// 比较输入参数 Key 和当前节点 Key 的大小
cmp = cpr.compare(key, t. key);
// 参数的 Key 更小,向左面走,把当前接待你引用移动到他的左子节点上
if (cmp < 0)
t = t. left;
// 新插入的key大的话,则以当前节点的右孩子节点为新的比较节点
else if (cmp > 0)
t = t. right;
else
// 如果当前节点的key和新插入的key相等的话,则覆盖map的value,返回
return t.setValue(value);
// 如果没有相等的 Key, 一直会遍历到 NIL 节点为止
} while (t != null);
}
//在没有指定比较器的情况下, 调用自然排序的 Comparable 比较
else {
// 这里要求key不能为空,并且必须实现Comparable接口
if (key == null)
throw new NullPointerException();
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);
}

// 找到新节点的父节点后,创建节点对象
Entry<K,V> e = new Entry<K,V>(key, value, parent);
// 如果新节点key的值小于父节点key的值,则插在父节点的左侧
if (cmp < 0)
parent. left = e;
// 如果新节点key的值大于父节点key的值,则插在父节点的右侧
else
parent. right = e;
// 插入新的节点后,为了保持红黑树平衡,对红黑树进行调整(重新着色、旋转操作)
fixAfterInsertion(e);
// map元素个数+1
size++;
modCount++;
return null;
}

重点:fixAfterInsertion() 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/** 新增节点后对红黑树的调整方法 */
private void fixAfterInsertion(Entry<K,V> x) {
// 虽然内部类 Entry 的属性 color 默认为黑色, 但新节点一律先赋值为红色
x. color = RED;

// 新节点是根节点或者其父节点(简称父亲)为黑色,
// 插入红色节点并不会破坏红黑树的性质,无需调整。
// x 值的改变已被特别标记为(*),改变的过程是在不断地向上游遍历(或内部调整)
// 直到父亲为黑色, 或者到达根节点
while (x != null && x != root && x. parent.color == RED) {
// 如果新插入节点x的父节点是祖父节点的左孩子
if (parentOf(x) == leftOf(parentOf (parentOf(x)))) {
// 取得新插入节点x的叔叔节点
Entry<K,V> y = rightOf(parentOf (parentOf(x)));
// 如果新插入x的父节点是红色-------------------①
if (colorOf(y) == RED) {
// 将x的父节点设置为黑色
setColor(parentOf (x), BLACK);
// 将x的叔叔节点设置为黑色
setColor(y, BLACK);
// 将x的祖父节点设置为红色
setColor(parentOf (parentOf(x)), RED);
// 将x指向祖父节点,如果x的祖父节点的父节点是红色,按照上面的步奏继续循环
x = parentOf(parentOf (x));
} else {
// 如果新插入x的叔叔节点是黑色或缺少,且x的父节点是祖父节点的右孩子-------------------②
if (x == rightOf( parentOf(x))) {
// 左旋父节点
x = parentOf(x);
rotateLeft(x);
}
// 如果新插入x的叔叔节点是黑色或缺少,且x的父节点是祖父节点的左孩子-------------------③
// 将x的父节点设置为黑色
setColor(parentOf (x), BLACK);
// 将x的祖父节点设置为红色
setColor(parentOf (parentOf(x)), RED);
// 右旋x的祖父节点
rotateRight( parentOf(parentOf (x)));
}
} else { // 如果新插入节点x的父节点是祖父节点的右孩子,下面的步奏和上面的相似,只不过左旋右旋的区分,不再细讲
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;
}

这里只展开说一下左旋:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 对红黑树的节点(x)进行左旋转
*
* 左旋示意图(对节点x进行左旋):
* px px
* / /
* x y
* / \ --(左旋)-- / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
*/
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
// 取得要选择节点p的右孩子
Entry<K,V> r = p. right;
// "p"和"r的左孩子"的相互指向...
// 将"r的左孩子"设为"p的右孩子"
p. right = r.left ;
// 如果r的左孩子非空,将"p"设为"r的左孩子的父亲"
if (r.left != null)
r. left.parent = p;

// "p的父亲"和"r"的相互指向...
// 将"p的父亲"设为"y的父亲"
r. parent = p.parent ;
// 如果"p的父亲"是空节点,则将r设为根节点
if (p.parent == null)
root = r;
// 如果p是它父节点的左孩子,则将r设为"p的父节点的左孩子"
else if (p.parent. left == p)
p. parent.left = r;
else
// 如果p是它父节点的左孩子,则将r设为"p的父节点的左孩子"
p. parent.right = r;
// "p"和"r"的相互指向...
// 将"p"设为"r的左孩子"
r. left = p;
// 将"p的父节点"设为"r"
p. parent = r;
}
}
调整图示
调整图示

具体例子请看《码出高效》P197-P199

更多数据结构

请访问我的博客-数据结构分类