红黑树算法笔记
文章目录
- 红黑树 (Red-Black Tree) 学习笔记
- 0. 节点结构与哨兵节点
- 1. 什么是红黑树?
- 2. 红黑树的五个核心性质
- 3. 为什么需要红黑树?
- 4. 红黑树的基本操作
- a. 查找 (Search)
- b. 插入 (Insert)
- c. 删除 (Delete)
- 5. 维护平衡的关键操作
- a. 变色 (Recoloring)
- b. 旋转 (Rotations)
- 6. 红黑树的优缺点
- 优点:
- 缺点:
- 7. 应用场景
红黑树 (Red-Black Tree) 学习笔记
0. 节点结构与哨兵节点
在深入之前,我们先定义红黑树的节点结构,并引入一个重要的概念:哨兵节点 (Sentinel Node)。
- 颜色 (Color):
RED
或BLACK
- 键 (Key): 节点存储的值
- 左孩子 (Left): 指向左子节点
- 右孩子 (Right): 指向右子节点
- 父节点 (Parent): 指向父节点
哨兵节点 (NIL):
为了简化红黑树操作的边界条件处理,我们通常使用一个特殊的哨兵节点 NIL
来代表所有的叶子节点(外部节点)以及根节点的父节点。
NIL
节点的颜色总是 黑色。NIL
节点的key
,left
,right
,parent
字段可以不关心或指向自身。- 所有“真正的”叶子节点的
left
和right
指针都指向这个唯一的NIL
哨兵节点。
enum Color { RED, BLACK };struct Node {int key;Color color;Node *parent;Node *left;Node *right;// Constructor for a new node (usually red)Node(int k, Color c = RED, Node* p = nullptr, Node* l = nullptr, Node* r = nullptr): key(k), color(c), parent(p), left(l), right(r) {}
};// Global sentinel node
Node* NIL; // Should be initialized once, e.g., in the RBT class constructor// NIL = new Node(0, BLACK); NIL->parent = NIL; NIL->left = NIL; NIL->right = NIL;// Or make it a static member of a RBT class.
在后续伪代码中,当提到叶节点或空指针时,通常指的就是这个 NIL
哨兵节点。
1. 什么是红黑树?
红黑树是一种自平衡的二叉查找树(Self-Balancing Binary Search Tree)。它通过在每个节点上增加一个额外的颜色属性(红色或黑色)并遵循一组特定的规则来确保树在插入和删除操作后保持大致平衡,从而保证了在最坏情况下的操作时间复杂度为 O(log N),其中 N 是树中节点的数量。这种平衡不是绝对的(像 AVL 树那样左右子树高度差最多为1),而是通过性质保证最长路径不超过最短路径的两倍。
2. 红黑树的五个核心性质
红黑树必须始终满足以下五个性质:
- 性质1 (颜色): 每个节点要么是红色,要么是黑色。
- 性质2 (根节点): 根节点是黑色的。
- 性质3 (叶节点): 所有叶节点(NIL 节点,即空节点或外部节点)都是黑色的。在实现中,这些通常是哨兵节点
NIL
。 - 性质4 (红色节点): 如果一个节点是红色的,则它的两个子节点都是黑色的。(即,不能有两个连续的红色节点,从根到叶的路径上红色节点不相邻)。
- 性质5 (黑色高度): 对每个节点,从该节点到其所有后代叶节点(NIL节点)的简单路径上,均包含相同数目的黑色节点。这个数目被称为节点的“黑色高度 (black-height)”。
推论:
- 从根到叶子的最长路径(红黑交替)最多是最短路径(全黑)的两倍长。
- 一棵有
n
个内部节点的红黑树的高度h <= 2 * log2(n+1)
。
3. 为什么需要红黑树?
- 普通二叉查找树 (BST) 的问题: 在极端情况下(例如,按顺序插入已排序的数据),BST 可能退化成链表,导致查找、插入、删除操作的时间复杂度变为 O(N)。这使得 BST 在动态数据集上的性能不可靠。
- 红黑树的保证: 红黑树通过上述五个性质,确保树的高度始终保持在对数级别,即
O(log N)
。这使得即使在最坏情况下,其各项操作(查找、插入、删除)也能保持高效。 - 与 AVL 树的比较:
- 平衡性: AVL 树是更严格平衡的(左右子树高度差不超过1),红黑树则相对宽松(最长路径不超过最短路径两倍)。
- 操作效率:
- 查找: AVL 树通常更快,因为它更平衡,树高更低。
- 插入/删除: 红黑树通常更快。AVL 树为了维持严格平衡可能需要进行多次旋转(最多
O(log N)
次),而红黑树的插入/删除操作中,旋转次数是常数级的(插入最多2次,删除最多3次),变色操作可能是O(log N)
次。因此,在写操作频繁的场景下,红黑树可能更有优势。
- 实现复杂度: 两者都比较复杂。红黑树的修复情况(cases)比 AVL 树多,但每次修复涉及的旋转次数少。
4. 红黑树的基本操作
a. 查找 (Search)
与普通二叉查找树的查找操作完全相同。从根节点开始,比较目标值与当前节点键值,决定向左子树还是右子树继续查找,直到找到目标节点或到达 NIL
节点。
时间复杂度:O(log N),因为树高是对数级别的。
Node* search(Node* root, int key) {Node* current = root;while (current != NIL && current->key != key) {if (key < current->key) {current = current->left;} else {current = current->right;}}return current; // Returns NIL if not found
}
b. 插入 (Insert)
插入操作分为两步:
- 标准 BST 插入: 像在普通二叉查找树中一样找到新节点的插入位置,并插入新节点。
- 着色和修复:
- 着色: 新插入的节点
z
总是被初始化为 红色。- 原因:
- 如果设为黑色,几乎肯定会违反性质5 (黑色高度),因为会增加一条路径上的黑色节点数,而其他路径不变。修复性质5通常比修复性质4复杂。
- 设为红色,性质5天然满足(新红节点不改变任何路径的黑色节点数)。可能违反的性质是:
- 性质2:如果
z
是根节点且为红色 (简单修复:将其变黑)。 - 性质4:如果
z
的父节点z->parent
也是红色 (需要更复杂的修复)。
- 性质2:如果
- 原因:
- 修复 (Fixup): 调用一个修复过程
insert_fixup(z)
,通过一系列的变色 (Recoloring) 和 旋转 (Rotations) 操作来恢复红黑树的性质。修复过程从新插入的节点z
开始,向上回溯,直到所有性质都满足。
- 着色: 新插入的节点
插入修复 (insert_fixup(z)
) 逻辑:
循环条件:只要 z
不是根节点,且 z
的颜色是红色,并且 z->parent
的颜色也是红色 (违反性质4)。
令 p = z->parent
(父节点),g = p->parent
(祖父节点,此时 p
为红色,g
必然存在且为黑色,否则在 p
插入时就已违反性质4)。
令 u
为 z
的叔叔节点 (u = (p == g->left) ? g->right : g->left
)。
-
Case 1:
z
的叔叔u
是红色。- 动作:
- 将
p
(父) 设为黑色。 - 将
u
(叔) 设为黑色。 - 将
g
(祖父) 设为红色。 - 将当前节点
z
设为g
(z = g
)。
- 将
- 解释: 通过将
p
和u
变黑,g
变红,我们将问题向上推移了两层。g
变成红色可能会与g
的父节点形成新的红-红冲突,所以需要继续对g
进行检查。这条路径的黑色高度不变。
G(B) G(R) <-- z moves here for next iteration/ \ / \P(R) U(R) ---> P(B) U(B)/ /z(R) z(R) (original z)
- 动作:
-
Case 2:
z
的叔叔u
是黑色 (或 NIL),且z
是p
的内侧孩子 (Zig-Zag 情况)。
(例如,p
是g
的左孩子,z
是p
的右孩子;或者p
是g
的右孩子,z
是p
的左孩子)- 动作:
- 如果
z == p->right
且p == g->left
(左-右情况): 对p
左旋。然后z
变成原来的p
,原来的p
变成z
的左孩子。 - 如果
z == p->left
且p == g->right
(右-左情况): 对p
右旋。然后z
变成原来的p
,原来的p
变成z
的右孩子。 - 在旋转后,
z
和p
交换了角色。将z
指向新的父节点 (原来的p
)。
- 如果
- 解释: 这一步是为了将 Zig-Zag 情况转换为 Zig-Zig 情况 (Case 3),方便后续处理。旋转后,新的
z
(原来的p
) 和其父节点 (原来的z
) 以及祖父节点g
会形成一条直线。 - 现在
z
是外侧孩子,进入 Case 3。
(Example: p is left child, z is right child of p)G(B) G(B)/ \ / \P(R) U(B) --L_ROT(P)--> z(R) U(B) <-- old p becomes child of z/ \ / \ <-- z becomes new p for Case 3z(R) P(R)/(old z's left child if any)
- 动作:
-
Case 3:
z
的叔叔u
是黑色 (或 NIL),且z
是p
的外侧孩子 (Zig-Zig 情况)。
(例如,p
是g
的左孩子,z
是p
的左孩子;或者p
是g
的右孩子,z
是p
的右孩子)- 动作:
- 将
p
(父) 设为黑色。 - 将
g
(祖父) 设为红色。 - 如果
z == p->left
(左-左情况): 对g
右旋。 - 如果
z == p->right
(右-右情况): 对g
左旋。
- 将
- 解释: 通过变色和一次旋转,红-红冲突被解决。旋转后,原来的
p
成为子树的根,颜色为黑,其子节点z
(原当前节点) 和g
(原祖父) 均为红色,满足性质4。由于p
替代了原来黑色的g
的位置,路径的黑色高度也得以保持。调整完成。
(Example: p is left child, z is left child of p - Left-Left)G(B) P(B)/ \ / \P(R) U(B) --R_ROT(G)--> z(R) G(R)/ \z(R) U(B)
- 动作:
循环结束后:
最后,无论如何,将根节点 root
设为黑色 (满足性质2)。
// In RBT class
// Node* root; (initialized to NIL in constructor)
// Node* NIL; (global or static member, initialized)void left_rotate(Node* x); // See section 5
void right_rotate(Node* x); // See section 5void insert_fixup(Node* z) {Node* y; // Unclewhile (z->parent->color == RED) { // Parent is red (z is also red initially)if (z->parent == z->parent->parent->left) { // Parent is a left childy = z->parent->parent->right; // Uncleif (y->color == RED) { // Case 1: Uncle is REDz->parent->color = BLACK;y->color = BLACK;z->parent->parent->color = RED;z = z->parent->parent; // Move z up to grandparent} else { // Uncle is BLACKif (z == z->parent->right) { // Case 2: z is a right child (Zig-Zag LR)z = z->parent;left_rotate(z);}// Case 3: z is a left child (Zig-Zig LL, or after Case 2 transform)z->parent->color = BLACK;z->parent->parent->color = RED;right_rotate(z->parent->parent);}} else { // Parent is a right child (symmetric to above)y = z->parent->parent->left; // Uncleif (y->color == RED) { // Case 1: Uncle is REDz->parent->color = BLACK;y->color = BLACK;z->parent->parent->color = RED;z = z->parent->parent;} else { // Uncle is BLACKif (z == z->parent->left) { // Case 2: z is a left child (Zig-Zag RL)z = z->parent;right_rotate(z);}// Case 3: z is a right child (Zig-Zig RR, or after Case 2 transform)z->parent->color = BLACK;z->parent->parent->color = RED;left_rotate(z->parent->parent);}}}root->color = BLACK; // Ensure root is black
}void insert_node(int key) {Node* z = new Node(key, RED, NIL, NIL, NIL); // New node is REDNode* y = NIL; // trailing pointerNode* x = root; // current pointerwhile (x != NIL) {y = x;if (z->key < x->key) {x = x->left;} else {x = x->right;}}z->parent = y;if (y == NIL) { // Tree was emptyroot = z;} else if (z->key < y->key) {y->left = z;} else {y->right = z;}// z->left and z->right are already NIL from constructorinsert_fixup(z);
}
c. 删除 (Delete)
删除操作是红黑树中最复杂的操作。
- 标准 BST 删除:
- 首先找到要删除的节点
z
。 - 如果
z
最多只有一个孩子:直接删除z
,用其唯一的孩子(或NIL
)替换它。 - 如果
z
有两个孩子:找到z
的中序后继y
(即z
右子树中的最小节点)。用y
的键值替换z
的键值,然后问题转化为删除节点y
。此时y
最多只有一个右孩子 (因为y
是其子树中最小的,所以它没有左孩子)。
- 首先找到要删除的节点
- 实际被物理删除的节点
y
和其替代者x
:- 经过上述步骤,我们总能确定一个节点
y
,它是实际要从树中物理移除的节点。y
最多只有一个非NIL
的子节点。 - 令
x
为y
的那个唯一的孩子(如果存在),或者NIL
(如果y
没有孩子)。x
将会取代y
在树中的位置。
- 经过上述步骤,我们总能确定一个节点
- 修复 (
delete_fixup(x)
):- 如果被删除的节点
y
的颜色是 红色:- 删除红色节点
y
不会违反任何红黑树性质。红色节点不影响黑色高度,删除它也不会造成红-红冲突(因为它的子节点x
必然是黑色,根据性质4)。所以操作完成。
- 删除红色节点
- 如果被删除的节点
y
的颜色是 黑色:- 删除黑色节点
y
会导致经过y
的路径上的黑色节点数量减少1,从而违反性质5 (黑色高度)。 - 同时,如果
x
(替代者) 是红色且x
的父节点 (原来y
的父节点) 也是红色,则会违反性质4。 - 为了修复,我们视
x
为一个“额外”的黑色。如果x
原本是红色,它现在就变成了“红加黑”,即一个普通的黑色节点,修复完成。 - 如果
x
原本是黑色 (包括NIL
节点),它现在就变成了“双重黑色 (doubly black)”。这意味着x
所在位置需要一个额外的黑色来弥补被删除的y
的黑色。这时就需要调用delete_fixup(x)
。
- 删除黑色节点
- 如果被删除的节点
删除修复 (delete_fixup(x)
) 逻辑:
循环条件:只要 x
不是根节点,并且 x
的颜色是黑色 (表示它仍带有“双黑”属性)。
令 p = x->parent
。令 s
为 x
的兄弟节点。
-
Case 1:
x
的兄弟s
是红色。- 动作:
- 将
s
设为黑色。 - 将
p
(父) 设为红色。 - 如果
x
是p
的左孩子,对p
左旋;否则对p
右旋。 - 更新
s
为x
的新兄弟节点 (它现在是原来s
的一个孩子,并且是黑色的)。
- 将
- 解释: 这个操作将 Case 1 转换为 Case 2、3 或 4 中的一种,即
x
的兄弟是黑色的情况。旋转后x
的新兄弟是黑色的,并且x
的父节点p
变成了红色。路径的黑色高度不变。
(x is left child, s is red right child)P(B/R) S(B)/ \ / \x(DB) S(R) --L_ROT(P)--> P(R) S_child_R(B/R)/ \ / \S_L(B) S_R(B) x(DB) S_L(B) <-- s is now S_L
- 动作:
-
Case 2:
x
的兄弟s
是黑色,且s
的两个孩子都是黑色。- 动作:
- 将
s
(兄弟) 设为红色。 - 将当前节点
x
设为p
(父) (x = p
)。
- 将
- 解释: 将
s
变红,这样x
和s
子树的黑色高度都减少了1。这个“双黑”问题就被推给了父节点p
。如果p
原本是红色,现在变黑,问题解决。如果p
原本是黑色,它现在变成了新的“双黑”节点,继续循环。
P(B/R) P(was B -> DB, was R -> B)/ \ / \x(DB) S(B) ---> x(B) S(R)/ \ / \SL(B) SR(B) SL(B) SR(B)
- 动作:
-
Case 3:
x
的兄弟s
是黑色,s
的靠近x
的孩子是红色,s
的远离x
的孩子是黑色。
(例如,x
是左孩子,s->left
是红色,s->right
是黑色)- 动作:
- 将
s->left
(或s->right
,如果x
是右孩子) 设为黑色。 - 将
s
设为红色。 - 如果
x
是p
的左孩子,对s
右旋;否则对s
左旋。 - 更新
s
为x
的新兄弟节点 (它现在是原来s->left
或s->right
,并且是黑色的)。
- 将
- 解释: 这个操作将 Case 3 转换为 Case 4。旋转后,
x
的新兄弟节点是黑色的,并且其远离x
的孩子是红色的。
(x is left child)P(B/R) P(B/R)/ \ / \x(DB) S(B) ---> x(DB) SL(B) <-- new S/ \ \SL(R) SR(B) S(R)\SR(B)
- 动作:
-
Case 4:
x
的兄弟s
是黑色,且s
的远离x
的孩子是红色。
(例如,x
是左孩子,s->right
是红色)- 动作:
- 将
s
的颜色设为p
(父) 的颜色。 - 将
p
(父) 设为黑色。 - 将
s
的远离x
的孩子 (例如s->right
) 设为黑色。 - 如果
x
是p
的左孩子,对p
左旋;否则对p
右旋。 - 将
x
设为根节点 (这会终止循环)。
- 将
- 解释: 这是最终解决“双黑”问题的步骤。通过旋转和变色,额外的黑色被消除,并且所有红黑树性质都得到恢复。
p
的旋转将兄弟s
提升,其红色子节点用于平衡黑色高度。
(x is left child, s->right is red)P(ColorP) S(ColorP)/ \ / \x(DB) S(B) --L_ROT(P)--> P(B) SR(B)/ \ / \SL(B/R) SR(R) x(B) SL(B/R)
- 动作:
循环结束后:
如果 x
不是 NIL
,将 x
的颜色设为黑色。这可以安全地吸收任何剩余的“双黑” (如果 x
是根) 或确保性质。
辅助函数 transplant(u, v)
:
用节点 v
替换子树 u
。它负责更新 u
的父节点指向 v
,以及 v
的父节点指向 u
的父节点。
void transplant(Node* u, Node* v) {if (u->parent == NIL) {root = v;} else if (u == u->parent->left) {u->parent->left = v;} else {u->parent->right = v;}v->parent = u->parent; // Even if v is NIL, NIL->parent should be set
}Node* tree_minimum(Node* node) {while (node->left != NIL) {node = node->left;}return node;
}void delete_fixup(Node* x) {Node* s; // Siblingwhile (x != root && x->color == BLACK) {if (x == x->parent->left) { // x is left childs = x->parent->right;if (s->color == RED) { // Case 1: Sibling s is reds->color = BLACK;x->parent->color = RED;left_rotate(x->parent);s = x->parent->right; // Update sibling}// s is now guaranteed blackif (s->left->color == BLACK && s->right->color == BLACK) { // Case 2: Sibling's children are both blacks->color = RED;x = x->parent; // Move double blackness up} else {if (s->right->color == BLACK) { // Case 3: Sibling's near child red, far child blacks->left->color = BLACK;s->color = RED;right_rotate(s);s = x->parent->right; // Update sibling}// Case 4: Sibling's far child is reds->color = x->parent->color;x->parent->color = BLACK;s->right->color = BLACK;left_rotate(x->parent);x = root; // Problem solved, exit loop}} else { // x is right child (symmetric cases)s = x->parent->left;if (s->color == RED) { // Case 1s->color = BLACK;x->parent->color = RED;right_rotate(x->parent);s = x->parent->left;}if (s->right->color == BLACK && s->left->color == BLACK) { // Case 2s->color = RED;x = x->parent;} else {if (s->left->color == BLACK) { // Case 3s->right->color = BLACK;s->color = RED;left_rotate(s);s = x->parent->left;}// Case 4s->color = x->parent->color;x->parent->color = BLACK;s->left->color = BLACK;right_rotate(x->parent);x = root;}}}if (x != NIL) x->color = BLACK; // Ensure x is black (e.g. if it became root and was red)
}void delete_node_val(int key) {Node* z = search(root, key);if (z == NIL) return; // Node not foundNode* y = z; // y is the node to be physically removed or movedNode* x; // x is the child that replaces yColor y_original_color = y->color;if (z->left == NIL) {x = z->right;transplant(z, z->right);} else if (z->right == NIL) {x = z->left;transplant(z, z->left);} else {y = tree_minimum(z->right); // y is z's successory_original_color = y->color;x = y->right; // x is y's right child (y's left child is NIL)if (y->parent == z) {x->parent = y; // Important if x is NIL} else {transplant(y, y->right);y->right = z->right;y->right->parent = y;}transplant(z, y);y->left = z->left;y->left->parent = y;y->color = z->color;}// delete z; // Actual memory deallocation for z if it was a two-child case,// or if y == z. If y was successor, y got z's data and z is effectively// now where y was, and needs to be handled.// Proper memory management requires careful thought. For now, assume z// points to the node structure that is no longer part of the tree logic.if (y_original_color == BLACK) {delete_fixup(x);}// Don't forget to `delete` the node that was removed from the tree structure// For example, if y was z, then 'delete z' after transplant.// If y was z's successor, 'delete node_that_was_originally_y'// The logic above reuses y's node structure to replace z. The node physically// removed is the one that y was pointing to if y != z, or z itself if y == z.// A common implementation detail: the node to be deallocated is the one whose// content was overwritten (if successor copied) or the one directly removed.
}
5. 维护平衡的关键操作
a. 变色 (Recoloring)
改变节点的颜色(红色变黑色,黑色变红色)。这是最简单的操作,用于调整节点颜色以满足红黑树的性质,通常不改变树的结构,或者作为旋转操作的一部分。变色本身非常快,是 O(1) 操作。
b. 旋转 (Rotations)
旋转操作用于改变树的局部结构,重新组织节点间的父子关系,同时保持二叉查找树的性质(即中序遍历顺序不变)。旋转的目的是在不破坏 BST 性质的前提下,调整节点间的父子关系,以帮助恢复红黑树的性质(特别是性质4 - 无连续红节点,和性质5 - 黑色高度)。旋转是 O(1) 操作。
-
左旋 (Left Rotation) on node
x
:
假设x
有一个右孩子y
(y 不能是NIL
)。左旋使y
成为新的子树根,x
成为y
的左孩子。y
原来的左孩子B
成为x
的右孩子。Parent Parent| |x y/ \ / \A y --Left_Rotate(x)--> x C/ \ / \B C A B
步骤:
y = x->right
x->right = y->left
(将B
过继给x
)- 如果
y->left != NIL
, 则y->left->parent = x
y->parent = x->parent
(y
连接到x
的原父节点)- 如果
x->parent == NIL
(x
是根), 则root = y
- 否则,如果
x == x->parent->left
, 则x->parent->left = y
- 否则 (
x == x->parent->right
), 则x->parent->right = y
y->left = x
x->parent = y
-
右旋 (Right Rotation) on node
x
:
假设x
有一个左孩子y
(y 不能是NIL
)。右旋使y
成为新的子树根,x
成为y
的右孩子。y
原来的右孩子B
成为x
的左孩子。Parent Parent| |x y/ \ / \y C --Right_Rotate(x)--> A x/ \ / \A B B C
步骤 (对称于左旋):
y = x->left
x->left = y->right
(将B
过继给x
)- 如果
y->right != NIL
, 则y->right->parent = x
y->parent = x->parent
- 如果
x->parent == NIL
, 则root = y
- 否则,如果
x == x->parent->right
, 则x->parent->right = y
- 否则 (
x == x->parent->left
), 则x->parent->left = y
y->right = x
x->parent = y
// In RBT class
// Node* root;
// Node* NIL;void left_rotate(Node* x) {Node* y = x->right; // Set yx->right = y->left; // Turn y's left subtree into x's right subtreeif (y->left != NIL) {y->left->parent = x;}y->parent = x->parent; // Link x's parent to yif (x->parent == NIL) {root = y;} else if (x == x->parent->left) {x->parent->left = y;} else {x->parent->right = y;}y->left = x; // Put x on y's leftx->parent = y;
}void right_rotate(Node* x) {Node* y = x->left; // Set yx->left = y->right; // Turn y's right subtree into x's left subtreeif (y->right != NIL) {y->right->parent = x;}y->parent = x->parent; // Link x's parent to yif (x->parent == NIL) {root = y;} else if (x == x->parent->right) {x->parent->right = y;} else {x->parent->left = y;}y->right = x; // Put x on y's rightx->parent = y;
}
6. 红黑树的优缺点
优点:
- 性能保证: 所有基本操作(查找、插入、删除)在最坏情况下的时间复杂度都是 O(log N)。这是其最重要的特性。
- 相对高效的插入/删除: 相较于 AVL 树(另一种自平衡 BST),红黑树在插入和删除时需要的旋转次数较少(AVL 树可能需要
O(log N)
次旋转,红黑树插入最多2次,删除最多3次旋转)。变色操作虽然可能沿着路径向上传播,但总体上写操作的平均常量因子较低。 - 广泛应用: 由于其稳定的性能和相对 AVL 树更优的写操作效率,许多标准库和系统组件都采用红黑树。
- 空间开销小: 每个节点只需要额外存储一位颜色信息(1 bit)。
缺点:
- 实现复杂: 相较于普通 BST,红黑树的插入和删除操作涉及到多种情况的判断、变色和旋转,实现起来要复杂得多,容易出错。删除操作尤其复杂。
- 查找性能略逊于 AVL 树: 由于红黑树的平衡性不如 AVL 树严格(红黑树的最长路径可以是最短路径的2倍,AVL树是高度差最多1),其树高可能略大于 AVL 树。因此,在纯查找密集型应用中,AVL 树理论上可能略快一点(但两者都是 O(log N) 级别,实际差异通常不大)。
- 理解难度: 各种修复情况的逻辑和它们如何维持红黑性质的证明比较微妙,学习曲线较陡峭。
7. 应用场景
红黑树因其高效和稳定的性能,在需要频繁进行查找、插入和删除操作的动态数据集合中得到广泛应用:
- 关联数组/映射表 (Associative Arrays/Maps):
- C++ STL:
std::map
,std::multimap
,std::set
,std::multiset
。 - Java:
java.util.TreeMap
,java.util.TreeSet
。
- C++ STL:
- 进程调度:
- Linux 内核的 Completely Fair Scheduler (CFS) 使用红黑树来管理任务队列,确保进程按虚拟运行时间公平地获得 CPU。
- 内存管理:
- 在某些操作系统的内核或用户态内存分配器中,用于管理空闲内存块,可以快速找到合适大小的块或合并相邻块。
- IO 多路复用:
- 如 Linux 的
epoll
在内核中可能使用红黑树来管理被监控的文件描述符集合,以便高效地添加、删除和查找事件。
- 如 Linux 的
- 数据库索引:
- 虽然 B树及其变种在磁盘存储的数据库中更常见,但某些内存数据库或特定类型的索引可能会使用红黑树或其变种。
- 几何计算:
- 例如,计算几何中的区间树 (Interval Tree) 可以基于红黑树构建。