前言
红黑树是一种二叉查找树,二叉查找树就不再赘述,分析性能时需要研究最差性能,一般的二叉查找树有时会退化成线性表,比如顺序插入时。那么就需要保证二叉查找树的平衡性,比如AVL树,但是要严格保证这种平衡需要的代价太高了,由此我们先引出2-3查找树。
2-3查找树
定义:
- 2-结点, 含有一个键(及对应的值)和两条链接,左链接指向的2-3子树中的键都小于该结点,右链接相反。
- 3-结点,含有两个键(以及对应的值)和三条链接,指向的子树与2-结点类似。
2-3树的查找就不用多说了,叙述一下如何插入。
2-3查找树的插入
插入总是在叶子结点发生,包括两种情况,插入的结点为2-结点或者3-结点,如果是前者,直接插入即可,如果为后者,先暂时形成4-结点,然后转换为3个2-结点。
此时又分两种情况,插入结点的父节点为2-结点或者3-结点,如果是前者,将转换形成的父结点插入之前的父结点。
如果父结点为3-结点,重复结点为2-结点的过程,那么父结点就会形成4结点,转换为3-结点,重复已上过程,直到不再形成4-结点,若直到根结点还是4-结点,将之转换为3个2-结点,此时树高加一。
将一个4-结点分解为2-3树可能有6中情况,此处不再赘述,可参考书中P273。
全局性质:
- 局部变换不会影响全局有序性和平衡性:任意空链接到根结点的路径长度都是相等的。
- 2-3查找树的增长是自下向上的。
红黑树
前文所述的2-3查找树实现起来需要维护两种不同类型的结点,实现操作并不统一,情况太多,比较麻烦。为此,我们需要“红黑二叉查找树”的数据结构来实现它。
基本思想
用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们讲树中的链接分为两种类型:红链接将两个2-结点连接起来构成一个3-结点,黑链接则是2-3树中的普通链接。确切的说,我们讲3-结点表示为由一条左斜的红色链接相连的两个2-结点。
- 红链接均为左连接
- 没有任何一个结点同时和两条红链接相连
- 该树是完美黑色平衡的
Node定义
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
Key key; //键
Value val; //值
Node left, right; //左右链接
int N; //这颗子树中的结点总数
boolean color; //由父结点指向它的链接的颜色
public Node(Key key, Value val, int N, boolean color) {
this.key = key;
this.val = val;
this.N = N;
this.color = color;
}
}
private boolean isRed(Node x) {
if (x == null) return false;
return x.color == RED;
}
转换操作
我们需要这些操作来保持红黑树的两个重要性质, 不存在两条连续的红链接和红色的右链接。
2-结点插入
我们向2-3树中插入新键时,总是插入到已经存在的叶子结点,或者是2-结点,或者是3-结点,所以在红黑树中新插入的结点总是红色的。
对于2-结点的插入直接形成3-结点,新键对于原有键或左边或右边。对应于红黑树,新键成为3-结点中原有键的左子树或者右子树,其中右子树的情况需要调整,因为我们规定只存在左斜的红色链接,用上文转换操作中的rotateLeft(Node h)即可
3-结点插入
3-结点的插入有3中情况,左中右。对应于红黑树中,会有一下这几种情况:
插入实现
public class RedBlackBST<Key extends Comparable<Key>, Value> {
private Node root;
private class Node //见前文
private boolean isRed(Node h) //见前文
private Node rotateLeft(Node h)
private Node rotateRight(Node h)
private void flipColors(Node h)
private int size() {
return size(root);
}
private int size(Node h) {
if (h == null) return 0;
return h.N;
}
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) {
//没找到,新键结点,注意color为RED
return new Node(key, val, 1, RED);
}
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;
}
//2-结点右插入以及3-结点中插入的调整
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
//3-结点左插入的调整
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
//3-结点右插入以及3-结点左插入调整后的
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.N = size(h.left) + size(h.right) + 1;
return h;
}
红黑树的删除
首先,需要说明的是,2-3树是完美平衡的,即就是说,非叶子结点不存在空链接。
我们先来看看怎么实现deleteMin():
刚才说过非叶子结点不存在空链接,如果我们删除的2-结点,那么就麻烦了,不能直接删除,为什么?因为2-3树中最小的点是在左下角,如果这个结点是2-结点,直接删除的话,它的父结点会出现空链接,破坏了2-3树的完美平衡性。那么该怎么办?如果这个结点是3-结点,那么我直接删除就好了,所以我们可不可能把2-结点转换为3-结点?所以,在沿着左连接向下的过程中:
- 如果当前结点的左子结点不是2-结点,完成;
- 如果当前结点的左子结点是2-结点而它的亲兄弟结点不是2-结点,将左子结点的兄弟结点中的一个键移动到左子结点中(其实是将它移动到父亲节点,而父亲结点的键移动到左子结点);
- 如果当前结点的左子结点和它的亲兄弟结点都是2-结点,将它们三个合并为一个临时的4-结点,使父结点由3-结点变成2-结点或者由4-结点变成3-结点。(由上向下的过程我们允许临时生成4-结点,因为随后的由下向上的过程会分解)
deleteMin()实现
public Node deleteMin() {
if (!isRed(root.left) && !isRed(root.right)) {
root.color = RED;
}
root = deleteMin(root)
if (!isEmpty()) root.color = BLACK;
}
private Node deleteMin(Node h) {
if (h.left == null)
return null; //找到了最小的,删除
//如果当前结点不是3-结点并且它的左孩子不是一个3-结点,需要调整
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
//递归删除
h.left = deleteMin(h.left);
//借键以及融合过程中产生连续的以及右斜的红链接需要调整
return balance(h);
}
private Node moveRedLeft(Node h) {
//融合当前结点,左孩子,右孩子为新的4-结点
flipColors(h);
//如果右孩子是3-结点,这个时候需要上述的借键给左孩子
if (isRed(h.right.left)) {
h.right = rotateRight(h.right);
h.rotateLeft(h);
}
return h;
}
private Node balance(Node h) {
//右斜红链接调整
if (isRed(h.right)) h = rotateLeft(h);
//连续的红链接调整
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h)
//4-结点调整
if ()isRed(h.left && isRed(h.right)) flipColors(h);
return h;
}
deleteMax()实现
删除最大键的过程与删除最小键的过程类似,但有一点需要注意,树中红色链接都是左斜的。如果最大键在3-结点,不能直接删除,rotateRight(h)处理一下。我们可以在自上向下的过程中将根结点右子树的红链接右斜,然后递归返回时自下向上的修正即可。
public void deleteMax() {
if (!isRed(root.left) && !isRed(root.right)) {
root.color = RED;
}
root = deleteMax(root);
if (!isEmpty()) root.color = BLACK;
}
private Node deleteMax(Node h) {
//h的红链接右斜
if (isRed(h.left))
h = rotateRight(h);
if (h.right == null)
return null;
//h的红链接右斜,右孩子的红链接还是左斜的(如果有)
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
h.right = deleteMax(h.right);
return balance(h);
}
private Node moveRedRight(Node h) {
flipColors(h);
if (!isRed(h.left.left))
h = rotateRight(h);
return h;
}
delete()实现
在查找路径上进行与删除最小键相同的变换可以保证在查找过程中任意当前结点均不是2-结点。如果被查找的键在树的底部,可以直接删除。如果不在,需要将它与它的后继结点(即它的右子树的最小键)交换,因为是递归调用,删除之后回溯分解产生的不合法的红色链接。
public void delete(Key key) {
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = delete(root, key);
if (!isEmpty()) root.color = BLACK;
}
public Node delete(Node h, Key key) {
if (key,compareTo(h.key) < 0) {
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
h.left = delete(h.left, key);
} else {
//保证链接右斜,因为删除键在3-结点中的右边时需要调整
if (isRed(h.left))
h = rotateRight(h);
//找到了,并且在叶子结点可以直接删除,在向下查找的过程中,已经保证了结点不可能是2-结点
//只需要检查右孩子即可,因为如果左孩子非空,右孩子空只可能是3-结点,然而3-结点的红链接已经右斜了
if (key.compareTo(h.key) == 0 && (h.right == null))
return null;
//右子树遍历时保证结点非2-结点
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRIght(h);
//找到了,将结点替换为后继结点
if (key.compareTo(h.key) == 0) {
Node x = min(h.right);
h.key = x.key;
h.val = x.val;
h.right = deleteMin(h.right);
}
没找到,继续在右子树找
else h.right = delete(h.right, key);
}
//处理非法红色链接
return balance(h);
}
private Node min(Node h) {
if (h == null) return h;
else return min(h.left);
}
性能
红黑树的查找,插入均在log(n)级别