CC++ & Algorithm

红黑树的原理简介

极难6
语言版本:通用
概述:从“图书馆按编号找书”引出红黑树的概念,用通俗语言解释红黑树如何保持平衡,以及为什么 set/map 等关联容器选用它作为底层实现。

红黑树:让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条“交通规则”

红黑树是一棵二叉树,每个节点除了有值、左子树、右子树,还有一个颜色属性。它必须遵守以下规则(记不住没关系,理解精神就行):

  1. 每个节点要么红,要么黑。
  2. 根节点永远是黑色。
    → 就像书架的第一本书是黑色的,表示稳定。
  3. 所有叶子节点(NIL空节点)都是黑色。
    → 叶子节点是看不见的“虚拟节点”,但逻辑上存在,且都是黑色。
  4. 不能有两个连续的红色节点。
    → 红色节点后面不能紧跟红色节点,必须隔一个黑色。这就像红色书不能挨着放,中间必须有一本黑色书隔开。
  5. 从任意节点到它所有叶子节点的路径上,黑色节点的数量相同。
    → 这个数量叫“黑高”。就像每条路线上黑色书的本数一样多。

为什么要这样规定?因为有了规则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. 新手容易犯的几个错误

  1. 误以为红黑树是完全平衡的
    红黑树只是大致平衡,最长路径不超过最短的两倍。它不像 AVL 树那样严格平衡,但调整更少,总体性能更好。

  2. 认为插入新节点必须涂成黑色
    实际恰好相反——先涂红,再根据情况调整。涂黑可能会破坏黑高,更难修复。

  3. 混淆“红黑树”和“二叉搜索树”
    普通二叉搜索树没有颜色规则,可能变成链表。红黑树是二叉搜索树 + 颜色规则。

  4. 以为 set/map 的迭代器在插入后全部失效
    红黑树插入新元素后,除了被删除的节点,其他迭代器依然有效(因为节点内存地址没有变)。这一点比 vector 好很多。

  5. 在 Python 里找不到红黑树,就以为它没用
    Python 内置的 setdict 底层是哈希表,不是红黑树。如果需要有序集合,可以用 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,它实现了 SortedListSortedDict 等。


9. 相关指引:下一步学什么?

  • 自定义排序:set/map 默认用 < 比较,如果你想按自己的规则(比如按字符串长度、按成绩高低)排序,需要提供比较器。C++ 中可以通过仿函数或 lambda 实现。
  • AVL树:另一种自平衡二叉搜索树,平衡更严格,但调整更频繁。红黑树在实际中更常用。
  • 哈希表:了解 unordered_map/unordered_set 的原理(哈希函数、冲突解决、rehash)。
  • 迭代器失效规则:不同容器的迭代器在插入/删除后是否失效,是面试常问点。

红黑树是计算机科学中“以颜色换平衡”的智慧结晶。下次你用 setmap 时,可以想象后台有一位勤劳的“书架管理员”在默默旋转和着色,让一切快如闪电。

例题精讲

1单选题

红黑树中,红色节点的子节点必须是什么颜色?

A红色
B黑色
C可以是红色或黑色
D取决于父节点颜色
2判断题

红黑树的根节点可以是红色。

3填空题
红黑树插入新节点后,如果新节点的父节点是红色且叔叔节点也是红色,则执行的操作是:将父节点和叔叔节点变为黑色,将祖父节点变为红色,然后以___为当前节点继续检查。
4单选题

与AVL树相比,红黑树的平衡性要求更宽松,主要体现在?

A红黑树高度差不超过1
B红黑树允许所有路径黑色节点数不同
C红黑树允许最长路径不超过最短路径的2倍
D红黑树完全不需要旋转调整
5判断题

红黑树中,所有的叶子节点(即NIL节点)都是黑色的。