红黑树 (red-black tree) 是一种自平衡二叉树,于 1972 年由 Rudolf Bayer 发明,发明时被称为 对称二叉 B 树,现代名称红黑树来自 Knuth 的博士生 Robert Sedgewick 于 1978 年发表的论文。红黑树的结构复杂,但操作有着良好的最坏情况运行时间:它可以在 时间内完成查找、插入和删除操作。
红黑树是具有下列着色性质的 BST:
- 每个结点要么是黑色要么是红色
- 根是黑色的
- 如果一个结点是红色的,那么它的子结点必须是黑色的
- 从一个结点到一个 NULL 指针的每一条路径都必须包含相同数目的黑色结点
根据着色规则,red-black tree 高度最多是 ,因此查找保证是一种对数的操作。当然还有一条约定,空结点 nullptr 我们假设其为黑色,这样我们可以在不违反约定的情况下,方便操作。
通常困难在于将一个插入一个新结点后,如果将结点涂为黑色将违反性质 4,因为这会让路径上的黑色结点数量加一,但其他路径上黑色结点数量不变。
因此在插入结点时,默认结点为红色,父结点为黑色时,直接插入。在以下的情况中不再讨论这种情况。父结点是红色则会违反规则 3,在这种情况下必须修改树以满足所有性质。
红黑树的自底向上插入
如果新插入的结点 X 是红色的,它有父结点 P,兄弟结点 S,叔父结点 U,以及祖父结点 G。那么需要考虑几种情况:
- 如果 P、U 都是红色的,意味着 G 是黑色的,可以将 P、U 重绘为黑色将 G 重绘为红色,这样既不会违反规则 3 也不会违反规则 4。但是 G 之上的情况我们不知道,G 也可能是根,因此需要对 G 进行递归地向上进行重绘操作
- 如果 P 与 U 只有一个是红色的,意味着 G 是黑色的,而插入的 X 虽然不违反规则 4 但是违反了规则 3,迫使 X 或 P 变为黑色,而这样又会违反规则 4。为了让树再次符合要求,我们对其需要进行旋转操作并重绘结点颜色,其实这里的旋转操作与 AVL 树中是一致的,只是将结点的平衡因子转换为了颜色信息。 a. 当 X、P、G 形成 zig-zig 时,我们采用 single rotation b. 当 X、P、G 形成 zig-zag 时,我们采用 double rotation
// 该函数仅处理父结点是红色的情况,黑色情况则直接插入即可
// 使用头结点方便处理,头结点的 parent 指向 root,root 的 parent 指向 head
void insert_help(Node* node, Node* head) {
// 当结点不是树的根或父结点是红色时,进行循环
while (node != head->parent && node->parent->tag == RED) {
Node* uncle = get_uncle(node);
Node* grandparent = get_grandparent(node);
Node* parent = node->parent;
// 如果叔父结点不为空且为红色,符合情况 1
if (uncle != nullptr && uncle->tag == RED) {
parent->tag = uncle->tag = BLACK;
grandparent->tag = RED;
node = grandparent;
continue;
}
// 判断 zig-zig 或 zig-zag 类型,进行相应的旋转
if (parent == grandparent->left) {
if (node == parent->right) { // l-r 的 zig-zag
node = parent;
parent = rotate_left(parent);
}
rotate_right(grandparent); // l-l 的 zig-zig
} else {
if (node == node->parent->left) { // r-l 的 zig-zag
node = parent;
parent = rotate_right(parent);
}
ratate_left(grandparent); // r-r 的 zig-zig
}
grandparent->tag = RED;
parent->tag = BLACK;
}
head->parent->tag = BLACK;
}
红黑树的自顶向下插入
自底向上的操作需要父指针或栈保存路径,而自顶向下时实际是对红黑树应用自顶向下保证 S 不会时红色的过程。
在向下的过程中,如果结点 N 有两个红色的孩子时,将孩子重绘为黑色,结点 N 重绘为红色。结点 N 与其父结点 P 都为红色时,将违反红黑树的着色性质,此时对其进行 zig-zig 或 zig-zag 旋转即可。至于叔父结点 U 在自顶向下的过程中排除了红色的可能。
// 自顶向下插入,value 是待插入的值
void insert(Node* node, Node* head, T& value, Node** pos = nullptr) {
bool inserted = false;
if (node == nullptr) { // 插入结点,默认为红色
node = new Node(value);
*pos = node;
inserted = true;
}
if (node->left != nullptr && node->left->tag == RED &&
node->right != nullptr && node->right->tag == RED) {
node->left->tag = node->right->tag = BLACK;
node->tag = RED;
}
head->parent->tag = BLACK;
Node* gp = get_grandparent(node);
Node* parent = node->parent;
if (node->tag == RED && parent->tag == RED) {
// 判断 zig-zig 或 zig-zag 类型,进行相应的旋转
if (parent == gp->left) {
if (node == parent->right) { // l-r 的 zig-zag
parent = rotate_left(parent);
}
rotate_right(gp); // l-l 的 zig-zig
} else {
if (node == parent->left) { // r-l 的 zig-zag
parent = rotate_right(parent);
}
ratate_left(gp); // r-r 的 zig-zig
}
gp->tag = RED;
parent->tag = BLACK;
}
if (inserted) {
return;
}
if (node->val < value) {
insert(node->left, head, &node->left, value);
} else {
insert(node->right, head, &node->right, value);
}
}
红黑树的自顶向下删除
删除结点时,所有情况都可以归结于删除一个叶结点,因为删除带有两个孩子的结点,都可以与其左子树最大结点或右子树最小结点的值进行交换,这只改变了值没有改变颜色,并不影响红黑树的性质。之后删除交换后的叶结点即可。对于红色叶结点,我们可以将其直接删除,这不影响红黑树的结构,如果有孩子我们只需要用其孩子代替它即可。如此我们需要保证在自顶向下过程中保证叶结点是红色的。
假设当前结点是 N,其兄弟结点 S、父结点 P、叔父结点 U 和祖父结点 G。开始时需要将树根重涂为红色,沿树向下遍历,当到达一个结点时,确保 P 是红色、N 和 S 是黑色。在此过程中会遇到一些情况:
- N 有两个黑色的孩子,此时 a. S 也有两个黑色的孩子,那么重涂反转 N、S、P 的颜色,树结构不变 b. S 有红色的孩子,根据红色的孩子进行 signal rotate 或 double rotate。如果两个孩子都是红色,任选一个进行旋转即可
- N 有红色的孩子,此时向下递归 a. 新的 N 是红色,继续递归 b. 新的 N 是黑色,对 S 和 P 进行旋转,S 成为 P 的父结点,重绘 P 与 S 的颜色,即可得到红色的父结点 P。对于 P 来说,回到情况 1