前言

二叉查找树对于大多数情况下的查找和插入操作在效率上来说是没有问题的,但是在最差的情况下会达到线性级别,其效率取决于插入顺序。平衡查找树的数据结构能够保证在最差的情况下也能是对数级别,要实现这一目标我们需要保证树在插入完成之后始终保持平衡状态。

2-3查找树

在学习红黑树之前要先了解2-3查找树作为基础,一棵2-3查找树或为一棵空树,或由以下节点组成:

  • 2-节点:含有一个键值对和两条链接,左链接指向的2-3树中的键都小于该节点,右链接指向的2-3树中的键都大于该节点。
  • 3-节点:含有两个键值对和三条链接,左链接指向的2-3树中的键都小于该节点,中链接指向的2-3树中的键都位于该节点的两个键之间,右链接指向的2-3树中的键都大于该节点。

指向一棵空树的链接称为空链接,一棵完美平衡的2-3查找树的所有空链接到根节点的距离应该是相同的。

2-3

查找

要判断一个键是否在树中,我们先将它和根节点中的键比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这是个空链接,查找未命中。

search

插入

  • 如果插入到2-节点上,那么直接将新节点和原来的节点组成3-节点即可。

  • 如果是插入到3-节点上,就会产生一个临时4-节点时,需要将4-节点分裂成3个2-节点,并将中间的2-节点移到上层节点中。如果上移操作继续产生临时4-节点则一直进行分裂上移,直到不存在临时4-节点。

如果从插入节点到根节点的路径上全都是3-节点,我们的根节点最终变成一个临时的4-节点,此时我们将临时的4-节点分解为3个2-节点,使得树高加一。这次最后的变换仍然保持了树的完美平衡性,因为它变换的是根节点。

all

构造轨迹

二叉查找树是由上向下生长的,而2-3树的生长是由下向上的。

construct

红黑树

2-3查找树实现起来十分复杂,因此我们使用一种名为红黑二叉查找树的简单数据结构来表达并实现它。

我们将树中的链接分为两种类型:红链接将两个2-节点连接起来构成一个3-节点,黑链接则是2-3树中的普通链接。

redblack

红黑树有以下性质:

  • 红链接均为左链接。
  • 没有任何一个节点同时和两条红链接相连。
  • 红黑树是完美黑色平衡的,即任意空链接到根节点的路径上的黑链接数量相同。

如果我们将由红链接相连的节点合并,得到的就是一棵2-3树:

基本实现

我们将由父节点指向自己的链接的颜色保存在表示节点的Node数据类型的布尔变量Color中。如果是红色则为true,黑色则为false。

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

public class RedBlackBST<Key extends Comparable<Key>, Value> {
private static final boolean RED = true;
private static final boolean BLACK = false;

private Node root;

private class Node {
private Key key;
private Value val;
private Node left, right;
private boolean color; //由其父节点指向它的链接的颜色
private int size; //这棵子树中的节点总数

public Node(Key key, Value val, boolean color, int size) {
this.key = key;
this.val = val;
this.color = color;
this.size = size;
}
}

public int size() {
return size(root);
}

private int size(Node x) {
if (x == null) return 0;
return x.size;
}

private boolean isRed(Node x) {
if (x == null) return false;
return x.color == RED;
}

//实现见下文
private Node rotateLeft(Node h)

//实现见下文
private Node rotateRight(Node h)

//实现见下文
private void flipColors(Node h)
}

旋转

假设我们有一条红色的右链接需要被转化为左链接,我们要进行左旋转。同理,也有右旋转。

left right

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//左旋转
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = x.left.color;
x.left.color = RED;
x.size = h.size;
h.size = size(h.left) + size(h.right) + 1;
return x;
}

//右旋转
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = x.right.color;
x.right.color = RED;
x.size = h.size;
h.size = size(h.left) + size(h.right) + 1;
return x;
}

颜色转换

一个4-节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从2-3树的角度看就是将中间节点移到上层节点。

change

1
2
3
4
5
private void flipColors(Node h) {
h.color = !h.color;
h.left.color = !h.left.color;
h.right.color = !h.right.color;
}

插入

先将一个节点按二叉查找树的方法插入到正确位置,然后在沿着插入点到根节点的路径向上移动时,在所经过的每个节点中顺序完成如下操作:

  • 如果右子节点是红色的而左子节点是黑色的,进行左旋转;
  • 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转;
  • 如果左右子节点均为红色的,进行颜色转换,将红链接在树中向上传递。

insert

颜色转换会使根节点变为红色,但根节点并没有父节点,因此在每次插入后都将根节点设为黑色。注意,每当根节点由红变黑时树的黑链接高度就会加一,因为这意味着它由一个4-节点分裂出去成为2-节点了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void put(Key key, Value val) {
//查找key,找到则更新其值,否则为它新建一个节点
root = put(root, key, val);
root.color = BLACK;
}

private Node put(Node h, Key key, Value val) {
if (h == null) //标准的插入操作,和父节点用红链接相连
return new Node(key, val, RED, 1);

int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = put(h.left, key, val);
else if (cmp > 0) h.right = put(h.right, key, val);
else h.val = val;

if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.size = size(h.left) + size(h.right) + 1;

return h;
}

除了递归调用后的三条if语句,红黑树中put()的递归实现和二叉查找树中put()的实现完全相同。

insert

删除最小键

为保证树的完美平衡性,沿着左链接向下进行变换,确保不会删除一个2-节点。在最后得到的含有最小键的3-节点或4-节点中,我们可以直接将最小键删除,然后向上分解所有临时的4-节点。

删除最小键

删除

在查找路径上进行和删除最小键相同的变换同样可以保证在查找过程中任意当前节点均不是2-节点。如果被查找的键在树的底部,我们可以直接删除它。如果不在,我们需要将它和它的后继节点交换,就和二叉查找树一样。因为当前节点必然不是2-节点,问题已经转化为在一棵根节点不是2-节点的子树中删除最小的键,可以直接使用上文的算法。删除之后我们需要向上回溯并分解余下的4-节点。

查找

红黑树的get()方法不会检查节点的颜色,因此实现和二叉查找树一样,但由于树是平衡的,所以查找比二叉查找树更快。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Value get(Key key) {
return get(root, key);
}

private Value get(Node x, Key key) {
while (x != null) {
int cmp = key.compareTo(x.key);
if (cmp < 0) x = x.left;
else if (cmp > 0) x = x.right;
else return x.val;
}
return null;
}

复杂度分析

无论键的插入顺序如何,红黑树都几乎是完美平衡的,因此查找、插入等操作在最坏的情况下所需的时间仍是对数级别的。

参考资料