红黑树的原理简介
极难6红黑树:让set和map又快又稳的“自动平衡书架”
你有没有在图书馆找书的经历?成千上万本书如果随意乱放,找一本书就像大海捞针。但管理员按编号从小到大排列,你就可以用“先看中间,再决定向左还是向右”的方法快速找到。这就像计算机里的 二叉搜索树。不过,如果书不断被插入和拿走,原本整齐的队伍可能会歪成一长溜——比如总是插入很小的编号,书都挤在左边,找书又变成了从头翻到尾,效率很低。
为了解决这个问题,计算机科学家发明了一种 会自动整理书架的魔法——红黑树。它就像一位聪明的图书管理员,每插入或删除一本书后,都会迅速地调整一下书架,让书架始终保持“大致平衡”。这样,无论怎么折腾,你找书永远只需花很少的时间。
红黑树是C++里 set、map 等关联容器的底层实现,它保证了插入、删除、查找的速度都是 O(log n)——哪怕有100万本书,最多只需要20次比较。这篇文章就用小学生也能听懂的语言,带你揭开红黑树的秘密。
1. 从“歪书架”到“自平衡书架”
先来看看没有平衡的二叉搜索树会怎么样。
假设图书馆里书的编号是 1, 2, 3, 4, 5,管理员按照“左小右大”的规则摆放:
- 先放 1:放在中间。
- 再放 2:因为 2 > 1,放在1的右边。
- 接着放 3:3 > 1,往右;3 > 2,再往右……结果变成了 右斜链:1 → 2 → 3 → 4 → 5。
- 这时要找书 5,得从1、2、3、4一路找下去,和一本本翻没区别。
这就是二叉搜索树 退化成链表 的问题。红黑树通过给每个节点涂上 红色 或 黑色,并遵守5条简单的规则,来防止这种“歪楼”发生。
你可以把红色节点想象成“需要特别关照的书”,黑色节点是“普通书”。管理员通过调整这些书的位置(旋转)和重涂颜色,让书架保持平衡。
2. 红黑树的5条“交通规则”
红黑树是一棵二叉树,每个节点除了有值、左子树、右子树,还有一个颜色属性。它必须遵守以下规则(记不住没关系,理解精神就行):
- 每个节点要么红,要么黑。
- 根节点永远是黑色。
→ 就像书架的第一本书是黑色的,表示稳定。 - 所有叶子节点(NIL空节点)都是黑色。
→ 叶子节点是看不见的“虚拟节点”,但逻辑上存在,且都是黑色。 - 不能有两个连续的红色节点。
→ 红色节点后面不能紧跟红色节点,必须隔一个黑色。这就像红色书不能挨着放,中间必须有一本黑色书隔开。 - 从任意节点到它所有叶子节点的路径上,黑色节点的数量相同。
→ 这个数量叫“黑高”。就像每条路线上黑色书的本数一样多。
为什么要这样规定?因为有了规则4和5,就能保证 最长路径不会超过最短路径的两倍。最短路径全是黑色(黑高为h),最长路径红黑交替(最多2h),所以树的高度最多是 2 * log₂n,依然高效。
3. 平衡的秘密:为什么最长路径不超过两倍?
举个简单的例子:假设黑高是3(即从根到叶子有3个黑色节点)。那么:
- 最短路径:黑 → 黑 → 黑 → 叶(共3个黑)。
- 最长路径:黑 → 红 → 黑 → 红 → 黑 → 叶(红黑交替,红色不能连续)。
最长路径的节点数是 2×黑高 = 6,最短是 3,比例就是 2。
如果树有 n 个节点,黑高大约为 log₂n,树高就是 O(log n) 级别。这就是红黑树“大致平衡”的由来。
生活中的类比:你每天上学有两条路,一条全是柏油路(黑色),一条是柏油路和红砖路交替。两条路的路灯(黑色节点)数量一样多,但柏油路短,红砖路长,但最长也不会超过最短的两倍。这样无论走哪条,时间都在可控范围内。
4. 插入和删除时,管理员是怎么调整的?
当插入一个新节点时,管理员会先把它涂成 红色(因为红色比较好调整,不会破坏黑高)。然后检查是否违反了规则(比如出现两个红色相连)。如果违规,就通过 左旋、右旋 和 变色 来修复。
旋转是什么? 想象一下你把一本书从左边挪到右边,同时调整它的子书——就像一个体操动作:
- 左旋:某个节点(比如P)的右子节点(R)变成新父节点,P变成R的左子。
例如:P(5) 的右子 R(8),左旋后 R变成根,P变成R的左边。 - 右旋:反过来,左子节点变成父节点。
变色:把红色变成黑色,黑色变成红色。
这些调整只需要沿着树从下往上走几步,花费 O(log n) 时间。插入和删除后,树又变回合法状态。
注意:删除比插入复杂很多,但思想类似——通过旋转和变色维持平衡。你不需要死记旋转细节,只要知道“管理员会快速搞定”就行。
5. 为什么 set / map 选用红黑树?(vs 哈希表)
C++ 的 set 和 map 要求元素 有序,并且支持高效插入、删除、查找。红黑树正好满足:
- 有序性:中序遍历就是升序序列。
- 稳定高效:最坏情况也是 O(log n),不像哈希表可能退化到 O(n)。
- 迭代器稳定:插入其他元素时,已有迭代器通常仍有效(删除被删的节点会失效,但其他节点不受影响)。
- 不需要重哈希:哈希表当元素增多时需要扩容,重新分配内存,很慢;红黑树每次只影响局部节点。
对比哈希表(unordered_set/unordered_map):哈希表平均更快(O(1)),但无序,且最坏情况慢。如果你需要顺序遍历,或担心恶意输入导致哈希冲突,就用红黑树;如果只需要快速查找且不关心顺序,用哈希表更好。
6. 新手容易犯的几个错误
-
误以为红黑树是完全平衡的
红黑树只是大致平衡,最长路径不超过最短的两倍。它不像 AVL 树那样严格平衡,但调整更少,总体性能更好。 -
认为插入新节点必须涂成黑色
实际恰好相反——先涂红,再根据情况调整。涂黑可能会破坏黑高,更难修复。 -
混淆“红黑树”和“二叉搜索树”
普通二叉搜索树没有颜色规则,可能变成链表。红黑树是二叉搜索树 + 颜色规则。 -
以为 set/map 的迭代器在插入后全部失效
红黑树插入新元素后,除了被删除的节点,其他迭代器依然有效(因为节点内存地址没有变)。这一点比 vector 好很多。 -
在 Python 里找不到红黑树,就以为它没用
Python 内置的set和dict底层是哈希表,不是红黑树。如果需要有序集合,可以用bisect(但插入慢)或第三方库sortedcontainers。
7. 完整示例:用C++模拟一棵二叉搜索树(未平衡)
下面的代码展示了一个简单的二叉搜索树(没有红黑平衡),你可以看到插入元素后中序遍历是有序的。但如果插入顺序是升序,树就会变歪。真正红黑树代码太长,这里只演示基础结构。
#include <iostream>
using namespace std;
// 树节点:值、左子、右子(没有颜色)
struct TreeNode {
int val; // 节点值(书的编号)
TreeNode* left; // 左子节点(比当前小的书)
TreeNode* right; // 右子节点(比当前大的书)
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class BST {
public:
TreeNode* root; // 根节点
BST() : root(nullptr) {}
// 插入一个值
void insert(int x) {
root = insertHelper(root, x);
}
// 中序遍历(输出升序序列)
void inorder() {
inorderHelper(root);
cout << endl;
}
private:
// 递归插入(未做平衡,可能产生倾斜)
TreeNode* insertHelper(TreeNode* node, int x) {
if (node == nullptr) {
return new TreeNode(x);
}
if (x < node->val) {
node->left = insertHelper(node->left, x);
} else {
node->right = insertHelper(node->right, x);
}
return node; // 没有平衡处理!
}
void inorderHelper(TreeNode* node) {
if (node == nullptr) return;
inorderHelper(node->left); // 遍历左子树
cout << node->val << " "; // 输出当前节点
inorderHelper(node->right); // 遍历右子树
}
};
int main() {
BST tree;
// 插入一组数字:5,3,7,2,4,6,8(还算平衡)
tree.insert(5);
tree.insert(3);
tree.insert(7);
tree.insert(2);
tree.insert(4);
tree.insert(6);
tree.insert(8);
cout << "中序遍历结果(升序): ";
tree.inorder(); // 输出 2 3 4 5 6 7 8
// 如果按顺序插入 1,2,3,4,5,树会变成右斜链
BST skewTree;
for (int i=1; i<=5; ++i)
skewTree.insert(i);
cout << "倾斜树中序遍历: ";
skewTree.inorder(); // 输出 1 2 3 4 5(但树结构是链表)
return 0;
}
运行结果:
中序遍历结果(升序): 2 3 4 5 6 7 8
倾斜树中序遍历: 1 2 3 4 5
第一组数据比较均匀,第二组完全倾斜。红黑树能防止倾斜。
8. Python 中如何模拟有序集合?
Python 标准库没有红黑树,但可以用 bisect 模块在列表中维护有序序列(插入是 O(n))。下面代码演示了如何插入并保持有序:
import bisect
class SortedList:
"""用列表 + 二分查找模拟有序容器(不是红黑树,仅演示效果)"""
def __init__(self):
self._data = [] # 内部列表
def insert(self, value):
# 二分查找插入位置,然后插入(O(n) 时间)
bisect.insort(self._data, value)
def __repr__(self):
return str(self._data)
# 使用示例
s = SortedList()
for x in [5, 3, 7, 2, 4, 6, 8]:
s.insert(x)
print("有序列表:", s) # 输出 [2, 3, 4, 5, 6, 7, 8]
如果你想在 Python 中体验红黑树的效率,可以使用第三方库 sortedcontainers,它实现了 SortedList、SortedDict 等。
9. 相关指引:下一步学什么?
- 自定义排序:set/map 默认用
<比较,如果你想按自己的规则(比如按字符串长度、按成绩高低)排序,需要提供比较器。C++ 中可以通过仿函数或 lambda 实现。 - AVL树:另一种自平衡二叉搜索树,平衡更严格,但调整更频繁。红黑树在实际中更常用。
- 哈希表:了解 unordered_map/unordered_set 的原理(哈希函数、冲突解决、rehash)。
- 迭代器失效规则:不同容器的迭代器在插入/删除后是否失效,是面试常问点。
红黑树是计算机科学中“以颜色换平衡”的智慧结晶。下次你用 set 或 map 时,可以想象后台有一位勤劳的“书架管理员”在默默旋转和着色,让一切快如闪电。
例题精讲
红黑树中,红色节点的子节点必须是什么颜色?
红黑树的根节点可以是红色。
红黑树插入新节点后,如果新节点的父节点是红色且叔叔节点也是红色,则执行的操作是:将父节点和叔叔节点变为黑色,将祖父节点变为红色,然后以___为当前节点继续检查。与AVL树相比,红黑树的平衡性要求更宽松,主要体现在?
红黑树中,所有的叶子节点(即NIL节点)都是黑色的。