C++ 红黑树:从规则到实现,手把手带你写一棵红黑树

红黑树是二叉搜索树家族中重要的一员,在 C++ STL 的 mapset 底层、Linux 内核的调度器、Java 的 TreeMap 等地方都能看到它的身影。它通过一套精妙的颜色规则,在频繁的插入删除中维持着近似平衡,既保证了 O(log N) 的时间复杂度,又比 AVL 树拥有更少的旋转次数。


一、什么是红黑树

红黑树本质上是一棵二叉搜索树 ,但它的每个结点都增加了一个 颜色属性 ,只能是红色或黑色。通过下面四条严格的规则,红黑树能够保证:没有任何一条从根到叶子的路径会比其他路径长出 2 倍,从而实现近似平衡。

1.1 红黑树的四条铁律

  1. 结点非红即黑 --- 每个结点的颜色要么是红色,要么是黑色。

  2. 根结点必为黑色 --- 树的根结点始终是黑色的。

  3. 不连续红色 --- 如果一个结点是红色的,那么它的两个孩子都必须是黑色的。也就是说,任意一条从根到叶子的路径上,不会出现连续的两个红色结点。

  4. 黑高相同 --- 对于任意一个结点,从它到其所有后代叶子结点(NIL 或 NULL 结点)的简单路径上,黑色结点的数量必须相同。

补充:在一些经典教材(如《算法导论》)中,会把叶子结点(NIL)也视为外部结点并强制为黑色,这主要是为了让"路径"的定义更加一致。在实际编码中,我们通常用 NULL 作为结束标志,并假设它也符合黑色规则,不影响平衡的判断。

1.2 为什么最长路径不会超过最短路径的 2 倍?

这是红黑树最核心的平衡保证。我们可以从下图中的极端情况来理解:

  • 根据 规则 4 ,每条路径上的黑色结点数量相同,记作 bh(black height)。

  • 根据 规则 2 和规则 3 ,红色结点不能连续出现,因此路径中最多的红色结点就是和黑色结点交替排列,即最长路径由"黑---红---黑---红......"组成,长度最多为 2 * bh

  • 最短路径则全是黑色结点,长度为 bh

因此,任意一条路径长度 h 满足:bh <= h <= 2 * bh 。这就保证了整棵树的高度始终被控制在对数级别,从而保证了增删查改的时间复杂度都是 O(log N)

1.3 红黑树 vs AVL 树

AVL 树通过记录每个结点的平衡因子(左右子树高度差不超过 1)来严格控制平衡,因此查询性能非常极致,但插入和删除时可能需要更多的旋转来恢复平衡。

红黑树的设计更"宽容"一些:它不追求绝对平衡,而是保证最长路径不超过最短路径的 2 倍。这使得红黑树在插入相同数量结点时,旋转次数通常比 AVL 树少,也因此更适合插入、删除操作非常频繁的场景。


二、红黑树的结构定义

在代码实现中,我们采用 key-value 结构的泛型模板,同时为每个结点增加颜色枚举以及指向父亲的 _parent 指针,方便后续调整。

cpp 复制代码
// 颜色枚举
enum Colour {
    RED,
    BLACK
};

// 红黑树结点
template<class K, class V>
struct RBTreeNode {
    pair<K, V> _kv;
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;
    Colour _col;

    RBTreeNode(const pair<K, V>& kv)
        : _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) { }
};

// 红黑树类
template<class K, class V>
class RBTree {
    typedef RBTreeNode<K, V> Node;
public:
    // 插入、查找、验证等接口
    bool Insert(const pair<K, V>& kv);
    Node* Find(const K& key);
    bool IsBalance();

private:
    Node* _root = nullptr;
    // 旋转函数,与 AVL 树相同,只是不更新平衡因子
    void RotateL(Node* parent);
    void RotateR(Node* parent);
    // 验证辅助函数
    bool Check(Node* root, int blackNum, const int refNum);
};

三、红黑树的插入 ------ 核心难点

插入操作可以概括为以下几步:

  1. 按照二叉搜索树的规则 将新结点插入到正确位置。

  2. 新结点默认染成 红色。这是因为如果是黑色,一定会破坏规则 4(改变路径上的黑高),维护起来代价巨大;而插入红色结点只有可能破坏规则 3(连续红色),相对更容易修正。

  3. 如果父亲结点是黑色,直接结束,不需要任何调整。

  4. 如果父亲结点是红色(违反规则 3),则需要根据"叔叔结点"(即父结点兄弟)的颜色和状态,分三种情况处理。我们约定:c = 当前结点(cur),p = 父结点,g = 祖父结点,u = 叔叔结点。

3.1 情况一:叔叔存在且为红色 ------ 变色就能解决

p 红、u 红、g 黑时,我们只需:

  • pu 染黑;

  • g 染红;

  • 将当前处理结点 c 移动到 g,继续往上检查。

理解pu 变黑相当于在各自子树增加一个黑色结点,g 变红相当于维持原路径黑高不变。但 g 变红后可能与更上层的红结点冲突,因此需要循环向上更新。如果最后 g 是根,我们再强行把它染回黑色。

无论 p 位于 g 的左边还是右边,cp 的左还是右,处理方式完全一样,只涉及变色,不需要旋转。

cpp 复制代码
// 情况一:叔叔存在且为红
if (uncle && uncle->_col == RED) {
    parent->_col = uncle->_col = BLACK;
    grandfather->_col = RED;

    // 继续向上调整
    cur = grandfather;
    parent = cur->_parent;
}

3.2 情况二 + 情况三:叔叔不存在或为黑色 ------ 旋转+变色

u 不存在或颜色为黑,单纯的变色已经无法解决连续红色问题,这时候必须借助旋转。

根据 pc 的相对位置,又细分为单旋和双旋两种:

3.2.1 单旋场景(直线型)
  • pg 的左孩子,cp 的左孩子 → 对 g 进行 右单旋

  • pg 的右孩子,cp 的右孩子 → 对 g 进行 左单旋

旋转完毕后,p 染黑、g 染红 。此时 p 成为新子树的根,整体黑高不变,且解决了连续红色问题,不需要再向上迭代。

cpp 复制代码
// 情况二单旋:p 为 g 的左,c 为 p 的左
if (cur == parent->_left) {
    RotateR(grandfather);
    parent->_col = BLACK;
    grandfather->_col = RED;
}

// p 为 g 的右,c 为 p 的右
if (cur == parent->_right) {
    RotateL(grandfather);
    parent->_col = BLACK;
    grandfather->_col = RED;
}
3.2.2 双旋场景(折线型)
  • pg 的左孩子,cp 的右孩子 → 先对 p 左单旋,再对 g 右单旋;

  • pg 的右孩子,cp 的左孩子 → 先对 p 右单旋,再对 g 左单旋;

旋转后,c 染黑、g 染红 。此时 c 变成了新子树的根,同样黑高不变,且不需要继续向上调整。

cpp 复制代码
// 情况三双旋:p 为 g 的左,c 为 p 的右
else {
    RotateL(parent);
    RotateR(grandfather);
    cur->_col = BLACK;
    grandfather->_col = RED;
}

// p 为 g 的右,c 为 p 的左
else {
    RotateR(parent);
    RotateL(grandfather);
    cur->_col = BLACK;
    grandfather->_col = RED;
}

3.3 插入操作完整代码

结合以上所有情况,插入函数的伪代码框架如下:

cpp 复制代码
bool Insert(const pair<K, V>& kv) {
    // 1. 空树:新建黑结点作为根
    if (_root == nullptr) {
        _root = new Node(kv);
        _root->_col = BLACK;
        return true;
    }

    // 2. 二叉搜索树查找插入位置
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur) {
        parent = cur;
        if (kv.first < cur->_kv.first)
            cur = cur->_left;
        else if (kv.first > cur->_kv.first)
            cur = cur->_right;
        else
            return false;   // 已存在
    }

    // 3. 新建红色结点,挂在父结点下
    cur = new Node(kv);
    cur->_col = RED;
    if (kv.first < parent->_kv.first)
        parent->_left = cur;
    else
        parent->_right = cur;
    cur->_parent = parent;

    // 4. 调整红黑树
    while (parent && parent->_col == RED) {
        Node* grandfather = parent->_parent;
        if (parent == grandfather->_left) {
            Node* uncle = grandfather->_right;
            // 情况一:叔叔红 -> 变色
            if (uncle && uncle->_col == RED) {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            }
            else { // 叔叔黑或不存在
                if (cur == parent->_left) {
                    // 单旋右
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } else {
                    // 双旋左右
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;  // 旋转后结构稳定,可退出
            }
        } else {
            // 对称情况:parent 是祖父的右孩子
            Node* uncle = grandfather->_left;
            if (uncle && uncle->_col == RED) {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            } else {
                if (cur == parent->_right) {
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } else {
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        }
    }

    // 5. 强制根为黑
    _root->_col = BLACK;
    return true;
}

旋转函数与 AVL 树完全一致,只需要改变指针指向即可,这里不再赘述。

3.4 为什么"旋转+变色"后就可以直接退出?

因为经过单旋或双旋 后,新的子树根结点(被染黑的那个)代替了原来的 g,它的颜色一定是黑色。这样一来,新根与上层的颜色连接断然不会再出现"连续红色",整棵树的平衡已经恢复,所以可以 break,不再继续向上调整。


四、红黑树的查找

查找操作完全沿用二叉搜索树的特性,复杂度 O(log N)

cpp 复制代码
Node* Find(const K& key) {
    Node* cur = _root;
    while (cur) {
        if (key < cur->_kv.first)
            cur = cur->_left;
        else if (key > cur->_kv.first)
            cur = cur->_right;
        else
            return cur;
    }
    return nullptr;
}

五、红黑树的验证 ------ 你的树真的"红黑"吗?

写完插入后,我们需要一套可靠的验证工具,而不是凭感觉判断。直接套用四条规则:

  1. 颜色只能为红或黑 → 枚举保证了这一点。

  2. 根是黑色。

  3. 不能有连续红色结点 → 可以用前序遍历检查,反向检查父亲颜色更方便:若当前结点为红且父亲也为红,则违规。

  4. 每条路径黑高相同 → 先通过最左边一条路径统计出一个参考黑高 refNum,然后在前序遍历每条路径时,累计黑色结点数,走到空时对比。

cpp 复制代码
bool Check(Node* root, int blackNum, const int refNum) {
    if (root == nullptr) {
        // 一条路径走完,比较黑高
        if (blackNum != refNum) {
            cout << "存在黑色结点数量不相等的路径" << endl;
            return false;
        }
        return true;
    }

    // 检查连续红色
    if (root->_col == RED && root->_parent->_col == RED) {
        cout << root->_kv.first << " 存在连续红色结点!" << endl;
        return false;
    }

    if (root->_col == BLACK)
        blackNum++;

    return Check(root->_left, blackNum, refNum) &&
           Check(root->_right, blackNum, refNum);
}

bool IsBalance() {
    if (_root == nullptr) return true;
    if (_root->_col == RED) return false;  // 根非黑

    // 计算最左路径的黑高作为参考
    int refNum = 0;
    Node* cur = _root;
    while (cur) {
        if (cur->_col == BLACK) refNum++;
        cur = cur->_left;
    }

    return Check(_root, 0, refNum);
}

只要 IsBalance() 返回 true,就意味着我们的红黑树完全遵守了所有规则,平衡性自然就得到了保证。


六、红黑树的删除(了解)

红黑树的删除比插入更加复杂,涉及更多颜色的互换、兄弟结点的多重判断以及可能的二次调整。本文暂不作深入展开,感兴趣的同学可以阅读《算法导论》或《STL 源码剖析》中的相关章节。


七、总结

红黑树通过简单的颜色规则,以"不连续红""黑高相等"为约束,保证树的高度始终在 log N2log N 之间,从而获得稳定的 O(log N) 增删查改性能。它的实现核心在插入调整:

  • 叔叔红色:只变色,向上迭代;

  • 叔叔黑色/不存在 + 直线:单旋 + 变色,调整结束;

  • 叔叔黑色/不存在 + 折线:双旋 + 变色,调整结束。

与 AVL 树相比,红黑树的平衡条件更宽松,旋转次数更少,特别适合写多读多或频繁插入删除的场景。掌握红黑树,不仅加深了对自平衡搜索树的理解,更是窥见了许多工业级数据结构的底层设计哲学。


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区交流你的理解与困惑,我们一起进步!

相关推荐
lzh200409191 小时前
深入学习Linux进程间通信:解析消息队列
linux·c++
nlpming1 小时前
opencode SQLite 数据库结构与查询手册
算法
水饺编程1 小时前
第5章,[标签 Win32] :设备的尺寸(三)
c语言·c++·windows·visual studio
Cando学算法1 小时前
中位数定理:到所有点的距离之和最小的点就是中位数
c++·算法·学习方法
nlpming2 小时前
opencode 上下文压缩(Compaction)机制
算法
anew___2 小时前
算法刷题避坑指南:从数据规模到易错点的实战总结
算法
HZY1618yzh2 小时前
洛谷题解:P16304 [蓝桥杯 2026 省 Java C 组] 抽奖活动
java·c++·算法·蓝桥杯
智者知已应修善业2 小时前
【51单片机从奇数始再转偶数逐一点亮并循环】2023-9-8
c++·经验分享·笔记·算法·51单片机
倔强的猴子(翻版)2 小时前
我用 Python 写了个排序库,一亿数据量下比 C 级 np.sort() 快 7 倍
人工智能·python·算法·阿里云·文心一言