红黑树底层原理全解析:从 5 大性质到 STL 容器底层实现


🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《系统深入Linux操作系统》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言

在前面我们学习了二叉搜索树(BST),它能在 O(logN) 的平均时间复杂度下完成查找、插入和删除操作。但二叉搜索树有一个致命的缺陷------它可能退化成链表!

试想一下,如果插入的数据是 1, 2, 3, 4, 5, 6... 这样的有序序列,二叉搜索树就会变成一条"长蛇",每次操作都变成了 O(N) 的时间复杂度。

为了解决这个问题,计算机科学家们设计了两棵"自平衡"的二叉搜索树:

高度平衡条件 查找复杂度 调整代价
AVL树 任意节点左右子树高度差 ≤ 1 O(logN) 高(可能多次旋转)
红黑树 用颜色约束近似平衡 O(logN) 低(最多2次旋转)

红黑树是 STL 中 map、setmultimap、multiset 的底层结构,也是 Linux 内核中进程调度、内存管理等核心模块的数据结构。

文章目录


一、红黑树的5大性质

红黑树通过给节点标记"颜色"(红色或黑色)来约束树的高度,保证它始终近似平衡:

性质1: 每个节点要么是 红色,要么是 🟫黑色

性质2: 根节点必须是 🟫黑色

性质3: 每个 红色 节点的父节点必须是 🟫黑色(即不能有两个连续的红色节点)

性质4: 所有路径上的 🟫黑色节点数量必须相等****

性质5: 每个 空节点(NIL) 默认视为 🟫黑色

cpp 复制代码
enum Colour
{
    Red,
    Black
};

▲ 一棵合法的红黑树:每条路径上黑色节点数相同,无连续红节点


二、红黑树节点结构

红黑树的节点比普通二叉搜索树多了一个颜色字段:

cpp 复制代码
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)
    {
        // 新节点默认为红色(如果设为黑色会影响黑色高度平衡)
    }
};

为什么新插入的节点默认为红色?

  • 如果插入 红色 节点:只会可能违反"不能有连续红节点"这一条,调整代价较小
  • 如果插入 🟫黑色节点:必定会违反"路径黑色高度相等"这一条,影响更大****

所以插入 红色 节点,调整代价更小!


三、旋转操作------左旋与右旋

红黑树的平衡调整依赖两个基本操作------左旋右旋

3.1 右旋(RotateR)

将"左重"的树调整为平衡状态

cpp 复制代码
void RotateR(Node* parent)
{
    //     g              subL
    //   subL    →      p     g
    //     p                 subLR

    Node* cur = parent->_left;    // cur = p
    Node* subLR = cur->_right;   // subLR = p的右孩子
    Node* Pparent = parent->_parent; // 记下g的父节点

    // 1. 建立 p → g 的链接
    cur->_right = parent;
    parent->_parent = cur;

    // 2. 将 subLR 挂到 g 的左边
    parent->_left = subLR;
    if (subLR) subLR->_parent = parent;

    // 3. 处理 g 与其父节点的关系
    if (parent == _root)
    {
        _root = cur;
        cur->_parent = nullptr;
    }
    else
    {
        cur->_parent = Pparent;
        if (Pparent->_left == parent)
            Pparent->_left = cur;
        else
            Pparent->_right = cur;
    }
}

3.2 左旋(RotateL)

将"右重"的树调整为平衡状态(与右旋对称)

cpp 复制代码
void RotateL(Node* parent)
{
    //     g              subR
    //       subR   →  g     p
    //         p          subRL

    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* Pparent = parent->_parent;

    subR->_left = parent;
    parent->_parent = subR;
    parent->_right = subRL;
    if (subRL) subRL->_parent = parent;

    if (parent == _root)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        subR->_parent = Pparent;
        if (Pparent->_left == parent)
            Pparent->_left = subR;
        else
            Pparent->_right = subR;
    }
}

💡 Tip: 旋转操作只是改变指针链接,不破坏二叉搜索树的性质(中序遍历有序)


四、插入操作------4种情况详解

红黑树的插入是整个实现中最复杂的部分。我们需要 边插入边调整,保证每一步都满足5条性质。

插入的2个阶段

阶段1: 按照BST规则找到插入位置(与普通BST完全一样)

阶段2: 将新节点着色为 红色,然后从下往上检查和调整

4.1 调整的4种情况

cur = 新插入节点,parent = 父节点,grandfather = 祖父节点,uncle = 叔叔节点


🔹 情况一:父叔均为红(最简单)
复制代码
调整前:               调整后:
    g(黑)                 g(红)
   p(红) u(红)     →    p(黑)   u(黑)
   c(红)                  c(红)  ← 继续向上

策略: 父、叔变黑,祖父变红,cur 指向祖父继续向上检查


🔹 情况二:cur为红,uncle不存在或为黑 + cur是p的左孩子 → 单右旋
复制代码
调整前:                     调整后:
     g(黑)                     p(黑)
   p(红)   u(黑/空)    →    c(红)     g(红)
 c(红)                           u(黑/空)

策略: 对祖父右旋,然后父变黑、祖父变红,调整完成(break)


🔹 情况三:cur为红,uncle不存在或为黑 + cur是p的右孩子 → 先左旋后右旋
复制代码
调整前:                      先左旋(p):         再右旋(g):
     g(黑)                        g(黑)                c(黑)
   p(红)   u(黑/空)      →     c(红)  u(黑/空)   →   p(红)   g(红)
     c(红)                      p(红)                     u(黑/空)

策略: 先对父左旋(变成情况二),再对祖父右旋,cur变黑、祖父变红


🔹 情况四:父在祖父右侧(镜像对称)

镜像情况一:父叔均为红 → 父叔变黑,祖父变红,cur上移

镜像情况二:cur是p的右孩子 → 对祖父左旋,父变黑、祖父变红

镜像情况三:cur是p的左孩子 → 先对父右旋,再对祖父左旋


4.2 插入完整代码

cpp 复制代码
bool Insert(pair<K,V> kv)
{
    // ========== 阶段1:按BST规则找到插入位置 ==========
    if (_root == nullptr)
    {
        _root = new Node(kv);
        _root->_col = Black;  // 根节点必须为黑色
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;

    while (cur)
    {
        if (cur->_kv.first < kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            return false; // 键已存在,插入失败
        }
    }

    // ========== 阶段2:插入节点并调整 ==========
    cur = new Node(kv);
    cur->_col = Red; // 新节点默认为红色

    if (parent->_kv.first < kv.first)
        parent->_right = cur;
    else
        parent->_left = cur;
    cur->_parent = parent;

    // ========== 调整(从下往上) ==========
    while (parent && parent->_col == Red)
    {
        Node* grandfather = parent->_parent;

        // ========== 父在祖父左侧 ==========
        if (parent == grandfather->_left)
        {
            Node* uncle = grandfather->_right;

            // ✅ 情况一:父叔均为红 → 变色 + 上移
            if (uncle && uncle->_col == Red)
            {
                parent->_col = Black;
                grandfather->_col = Red;
                uncle->_col = Black;
                cur = grandfather;
                parent = cur->_parent;
            }
            // ❌ 情况二/三:uncle不存在或为黑
            else
            {
                // 情况二:cur是左孩子 → 单右旋
                if (cur == parent->_left)
                {
                    RotateR(grandfather);
                    parent->_col = Black;
                    grandfather->_col = Red;
                }
                // 情况三:cur是右孩子 → 先左旋再右旋
                else
                {
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = Black;
                    grandfather->_col = Red;
                }
                break; // 旋转后cur变黑,调整完成
            }
        }
        // ========== 父在祖父右侧(镜像处理)==========
        else
        {
            Node* uncle = grandfather->_left;

            if (uncle && uncle->_col == Red) // 镜像情况一
            {
                parent->_col = Black;
                grandfather->_col = Red;
                uncle->_col = Black;
                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;
            }
        }
    }

    _root->_col = Black; // 预防根节点变红
    return true;
}

五、平衡性检验

红黑树的平衡性检验分三步:

cpp 复制代码
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);
}

bool Check(Node* root, int blackNum, const int refNum)
{
    if (root == nullptr)
    {
        // 到达叶子(空节点),检查黑色节点数是否匹配
        if (refNum != blackNum)
        {
            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);
}

六、完整测试代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include"RBTree.h"

void TestRBTree1()
{
    RBTree<int, int> t;
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

    for (auto e : a)
    {
        t.Insert({ e, e });
    }

    t.InOrder();               // 中序遍历(应输出有序序列)
    cout << t.IsBalance() << endl; // 1 = 平衡,0 = 不平衡
}

int main()
{
    TestRBTree1();
    return 0;
}

七、红黑树 vs AVL树

对比维度 红黑树 AVL树
平衡标准 近似平衡(最长 ≤ 最短2倍) 严格平衡(左右高度差 ≤ 1)
插入调整 最多2次旋转 可能多次旋转
查找效率 O(logN)(实际略低) O(logN)(更接近logN)
适用场景 map/set(查询/插入混合) 数据库索引(查询多)
调整代价

总结: 红黑树牺牲了部分平衡性,换取了更低的调整代价,因此在实际工程中被广泛采用!


八、红黑树在STL中的应用

红黑树是 C++ STL 中关联式容器的底层结构:

STL容器 底层结构 是否允许键重复
map 红黑树
set 红黑树
multimap 红黑树
multiset 红黑树

正是因为红黑树的插入和删除调整代价低,所以 STL 选择了它作为底层结构!


本节完

作者:say-fall | 编辑:say-fall | 原创不易,如果对你有帮助,记得 点赞 + 收藏 哦!

相关推荐
摇滚侠2 小时前
Java 零基础全套视频教程,面向对象(进阶),笔记 90-103
java·开发语言·笔记
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 416. 分割等和子集 | C++ 0-1背包 1D空间极致优化
c++·算法·leetcode
青槿吖2 小时前
Sentinel 进阶实战:Feign 整合 + 全局异常 + Nacos 持久化,生产环境直接用
java·开发语言·spring cloud·微服务·云原生·ribbon·sentinel
AI技术社区2 小时前
Claude Code源码分析之提示词工程
java·开发语言·ai·ai编程
FL4m3Y4n2 小时前
分布式消息推送系统协议设计【C++ grpc kafka】
c++·分布式·kafka
riNt PTIP2 小时前
在21世纪的我用C语言探寻世界本质——字符函数和字符串函数(2)
c语言·开发语言
007张三丰2 小时前
系统架构设计师-以“云服务”主题为例的范文参考
java·开发语言·网络·软考高级·云服务·软考论文·论文范文
无限进步_2 小时前
二叉树的前序遍历(非递归实现)
开发语言·数据结构·c++·windows·git·visual studio
鬼蛟2 小时前
Sentinel
java·开发语言·数据库