红黑树的原理及实现

前言

红黑树是一种自平衡的二叉搜索树,通过对结点颜色的约束实现近似平衡,保证了增删查改的时间复杂度均为O(logN)。相较于严格平衡的 AVL 树,红黑树对平衡的控制更为宽松,插入操作的旋转次数更少,在实际工程中应用更为广泛(如 STL 中的 map、set 底层实现)。本文将从红黑树的核心概念、规则约束、实现细节等方面,全面讲解红黑树的原理与代码实现,帮助大家彻底掌握这一数据结构。

一、红黑树的核心概念

1.1 红黑树的定义

红黑树是一棵二叉搜索树,在普通二叉搜索树的基础上,为每个结点增加了一个颜色属性 (红色或黑色)。通过对从根到叶子的任意路径上的结点颜色 进行严格约束,红黑树确保了没有任何一条路径的长度会比其他路径长出 2 倍,从而实现了近似平衡

需要注意的是,红黑树中所指的叶子结点 并非传统二叉树的叶子结点,而是空结点(NIL/NULL),《算法导论》中明确将所有空结点定义为黑色,这一规则是为了方便统一计算路径上的黑色结点数量。

1.2 红黑树的五大核心规则

红黑树的所有特性均由以下 5 条规则约束(其中前 4 条为核心基础规则,第 5 条为补充规则):

  1. 每个结点的颜色只能是红色黑色
  2. 根结点必须是黑色
  3. 红色结点的两个孩子结点必须是黑色(即任意路径中无连续的红色结点);
  4. 对于任意一个结点,从该结点到其所有空结点的简单路径上,黑色结点数量完全相同
  5. 所有的空结点(NIL)均为黑色(补充规则,方便路径计算)。

1.3 红黑树的平衡原理:最长路径不超过最短路径 2 倍

红黑树通过上述规则,从数学上保证了最长路径长度 ≤ 2× 最短路径长度,推导过程如下:

  1. 由规则 4 可知,所有路径的黑色结点数量相同,记为黑高(bh)全黑路径是最短路径,长度为bh;
  2. 由规则 2 和 3 可知,路径中无连续红色结点,一黑一红交替的路径是最长路径,长度为2×bh。

由此可得红黑树的高度h满足:bh≤h≤2×bh,这也是红黑树 "近似平衡" 的本质。

1.4 红黑树的效率分析

假设红黑树的结点总数为N,黑高为bh,则结点数量满足:2bh−1≤N<22×bh−1,推导可得bh≈logN。红黑树的最坏高度为2×logN,因此增删查改操作的时间复杂度均为O(logN),与 AVL 树处于同一档次。但红黑树对平衡的控制更宽松,插入相同数量的结点时,红黑树的旋转次数远少于 AVL 树,这也是其在工程中更受欢迎的关键原因。

二、红黑树的结构设计

红黑树基于二叉搜索树实现,为了实现旋转和平衡调整,需要为每个结点增加父结点指针颜色属性 ,同时采用键值对(key/value) 结构以适配实际开发需求。

2.1 颜色枚举与结点结构体

首先定义结点的颜色枚举(RED/BLACK),再设计红黑树的结点结构体,包含键值对、左右孩子指针、父结点指针和颜色属性:

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

// 红黑树结点结构体(key/value结构)
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) // 默认构造为红色,插入时再根据情况调整
    {}
};

2.2 红黑树的主类结构

红黑树主类封装了根结点指针,对外提供插入、查找、验证等接口,内部实现旋转、平衡调整等核心逻辑:

复制代码
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:
    // 内部核心方法:旋转(与AVL树一致,无平衡因子)
    void RotateL(Node* parent); // 左单旋
    void RotateR(Node* parent); // 右单旋
    // 红黑树验证辅助函数
    bool Check(Node* root, int blackNum, const int refNum);

private:
    Node* _root = nullptr; // 红黑树根结点,初始为空
};

2.3 旋转操作实现

红黑树的旋转操作与 AVL 树完全一致,分为左单旋右单旋 ,目的是调整结点的位置,恢复红黑树的平衡,旋转操作仅改变结点的指向,不改变结点的颜色

右单旋(RotateR)

适用于 "左孩子的左子树过深" 的情况,以当前结点为旋转点,将其左孩子提升为新的根,原根成为新根的右孩子:

复制代码
template<class K, class V>
void RBTree<K, V>::RotateR(Node* g)
{
    Node* p = g->_left;
    Node* pRight = p->_right;

    // 处理p的右孩子
    g->_left = pRight;
    if (pRight)
        pRight->_parent = g;

    // 处理g的父结点
    Node* gParent = g->_parent;
    p->_parent = gParent;
    if (gParent == nullptr) // g是根结点
        _root = p;
    else if (g == gParent->_left) // g是左孩子
        gParent->_left = p;
    else // g是右孩子
        gParent->_right = p;

    // 完成旋转
    p->_right = g;
    g->_parent = p;
}
左单旋(RotateL)

适用于 "右孩子的右子树过深" 的情况,以当前结点为旋转点,将其右孩子提升为新的根,原根成为新根的左孩子:

复制代码
template<class K, class V>
void RBTree<K, V>::RotateL(Node* g)
{
    Node* p = g->_right;
    Node* pLeft = p->_left;

    // 处理p的左孩子
    g->_right = pLeft;
    if (pLeft)
        pLeft->_parent = g;

    // 处理g的父结点
    Node* gParent = g->_parent;
    p->_parent = gParent;
    if (gParent == nullptr) // g是根结点
        _root = p;
    else if (g == gParent->_left) // g是左孩子
        gParent->_left = p;
    else // g是右孩子
        gParent->_right = p;

    // 完成旋转
    p->_left = g;
    g->_parent = p;
}

三、红黑树的插入实现

红黑树的插入操作分为两步 :首先按二叉搜索树的规则 插入新结点,然后根据红黑树的规则调整结点颜色和结构 ,恢复平衡。插入是红黑树的核心难点,需根据叔叔结点的颜色分情况处理。

3.1 插入的基本规则

  1. 空树插入:新结点直接作为根结点,颜色设为黑色(满足规则 2);
  2. 非空树插入:新结点颜色必须设为红色(若设为黑色,会直接破坏规则 4,难以维护);
  3. 插入后检查:若新结点的父亲是黑色,无连续红色结点,满足所有规则,插入结束;若父亲是红色,违反规则 3,需根据叔叔结点的情况进行平衡调整。

为了方便描述,定义以下结点标识:

  • cur:新增结点(红色);
  • parent:cur的父结点(红色,违反规则 3 的前提);
  • grandfather:parent的父结点(黑色,由规则 3 推导,无连续红色结点);
  • uncle:parent的兄弟结点(关键变量,分情况的依据)。

3.2 插入的三种情况分析

插入后需处理的核心场景为:cur红、parent红、grandfather黑,此时根据uncle的状态分为三种情况,其中情况 1 仅变色,情况 2 和 3 需要旋转 + 变色

情况 1:uncle 存在且为红色(仅变色,无需旋转)

处理逻辑 :将parent和uncle设为黑色,grandfather设为红色,然后将grandfather作为新的cur,继续向上调整。原理

  • 变色后,grandfather所在子树的所有路径黑色结点数量不变,满足规则 4;
  • 消除了cur和parent的连续红色结点,满足规则 3;
  • 若grandfather的父结点为红色,会产生新的连续红色结点,因此需要继续向上调整;若grandfather成为根结点,最终需将其设为黑色。
情况 2:uncle 不存在 / 为黑色,且 cur 与 parent 同向(单旋 + 变色)

同向定义 :parent是grandfather的左孩子,cur是parent的左孩子;或parent是grandfather的右孩子,cur是parent的右孩子。处理逻辑

  1. 以grandfather为旋转点,进行单旋(左孩子同向则右单旋,右孩子同向则左单旋);
  2. 将parent设为黑色,grandfather设为红色;
  3. 调整结束,无需继续向上处理。原理:旋转后改变了结点的层级关系,变色后消除了连续红色结点,且所有路径的黑色结点数量不变,满足所有规则。
情况 3:uncle 不存在 / 为黑色,且 cur 与 parent 反向(双旋 + 变色)

反向定义 :parent是grandfather的左孩子,cur是parent的右孩子;或parent是grandfather的右孩子,cur是parent的左孩子。处理逻辑

  1. 以parent为旋转点,进行单旋(将反向转为同向,左孩子反向则左单旋,右孩子反向则右单旋);
  2. 以grandfather为旋转点,进行单旋(同情况 2 的单旋方向);
  3. 将新的根结点(原cur)设为黑色,grandfather设为红色;
  4. 调整结束,无需继续向上处理。原理:先通过一次单旋将 "反向" 转为 "同向",再按情况 2 处理,最终保证所有规则均被满足。

3.3 插入的完整代码实现

cpp 复制代码
template<class K, class V>
bool RBTree<K, V>::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)
    {
        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;
        }
    }

    // 3. 创建新结点,设为红色,建立父子关系
    cur = new Node(kv);
    cur->_col = RED;
    if (parent->_kv.first < kv.first)
        parent->_right = cur;
    else
        parent->_left = cur;
    cur->_parent = parent;

    // 4. 平衡调整:parent为红色时需要处理
    while (parent && parent->_col == RED)
    {
        Node* grandfather = parent->_parent;
        assert(grandfather && grandfather->_col == BLACK); // 由规则3推导,祖父必为黑

        // 情况:parent是祖父的左孩子
        if (parent == grandfather->_left)
        {
            Node* uncle = grandfather->_right;
            // 情况1:uncle存在且为红,仅变色
            if (uncle && uncle->_col == RED)
            {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                // 继续向上调整
                cur = grandfather;
                parent = cur->_parent;
            }
            // 情况2和3:uncle不存在/为黑,旋转+变色
            else
            {
                // 情况2:cur与parent同向,右单旋
                if (cur == parent->_left)
                {
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                // 情况3:cur与parent反向,双旋(左单旋+右单旋)
                else
                {
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                // 旋转后无需继续向上调整
                break;
            }
        }
        // 情况:parent是祖父的右孩子(与左孩子对称)
        else
        {
            Node* uncle = grandfather->_left;
            // 情况1:uncle存在且为红,仅变色
            if (uncle && uncle->_col == RED)
            {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                // 继续向上调整
                cur = grandfather;
                parent = cur->_parent;
            }
            // 情况2和3:uncle不存在/为黑,旋转+变色
            else
            {
                // 情况2:cur与parent同向,左单旋
                if (cur == parent->_right)
                {
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                // 情况3:cur与parent反向,双旋(右单旋+左单旋)
                else
                {
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                // 旋转后无需继续向上调整
                break;
            }
        }
    }

    // 最终保证根结点为黑色(防止祖父向上调整后根变为红色)
    _root->_col = BLACK;
    return true;
}

四、红黑树的查找实现

红黑树的查找操作与普通二叉搜索树完全一致,利用二叉搜索树 "左小右大" 的特性,从根结点开始依次比较键值,找到则返回结点指针,未找到则返回空。由于红黑树的高度为O(logN),因此查找的时间复杂度为O(logN)。

4.1 查找的完整代码实现

cpp 复制代码
template<class K, class V>
typename RBTree<K, V>::Node* RBTree<K, V>::Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < key)
        {
            // 键值更大,去右子树查找
            cur = cur->_right;
        }
        else if (cur->_kv.first > key)
        {
            // 键值更小,去左子树查找
            cur = cur->_left;
        }
        else
        {
            // 找到键值,返回结点指针
            return cur;
        }
    }
    // 未找到,返回空
    return nullptr;
}

五、红黑树的平衡验证

红黑树的平衡验证不能仅检查路径长度 (即使最长路径不超过最短路径 2 倍,也可能违反颜色规则),正确的方式是直接验证红黑树的 4 条核心规则,只要满足所有规则,红黑树必然是平衡的。

5.1 验证思路

  1. 验证规则 2:根结点必须是黑色;
  2. 验证规则 3:无连续的红色结点(反向检查:若当前结点为红色,其父结点不能为红色,更易实现);
  3. 验证规则 4:所有路径的黑色结点数量相同(前序遍历,记录根到当前结点的黑色结点数,走到空结点时对比参考值);
  4. 规则 1 由颜色枚举天然保证,无需验证。

5.2 平衡验证的完整代码实现

复制代码
// 辅助函数:前序遍历验证规则3和规则4
template<class K, class V>
bool RBTree<K, V>::Check(Node* root, int blackNum, const int refNum)
{
    // 走到空结点,验证当前路径的黑色结点数是否等于参考值
    if (root == nullptr)
    {
        if (blackNum != refNum)
        {
            cout << "错误:存在黑色结点数量不相等的路径" << endl;
            return false;
        }
        return true;
    }

    // 验证规则3:无连续的红色结点
    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);
}

// 对外接口:验证红黑树是否平衡
template<class K, class V>
bool RBTree<K, V>::IsBalance()
{
    // 空树视为平衡
    if (_root == nullptr)
        return true;

    // 验证规则2:根结点必须为黑色
    if (_root->_col == RED)
    {
        cout << "错误:根结点为红色" << endl;
        return false;
    }

    // 计算参考黑高:根到最左路径的黑色结点数
    int refNum = 0;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_col == BLACK)
        {
            refNum++;
        }
        cur = cur->_left;
    }

    // 递归验证所有规则
    return Check(_root, 0, refNum);
}
相关推荐
Jasmine_llq1 小时前
《B3953 [GESP202403 一级] 找因数》
算法·因数枚举算法(核心逻辑)·顺序遍历算法·单输入处理·逐行输出处理·整数算术运算
Bert.Cai1 小时前
Python time.sleep函数作用
开发语言·python
Eward-an2 小时前
【详细解析】删除有序数组中的重复项 II
数据结构·算法
sg_knight2 小时前
OpenClaw 能做什么?几个真实使用场景说明
算法·ai·大模型·llm·agent·openclaw·小龙虾
嫂子开门我是_我哥2 小时前
心电域泛化研究从0入门系列 | 第七篇:全流程闭环与落地总结——系列终篇
人工智能·算法·机器学习
爱学习的小囧2 小时前
零门槛!VCF 自动化环境登录 vSphere Supervisor 全教程
运维·服务器·算法·自动化·vmware·虚拟化
Book思议-2 小时前
线性表之顺序表入门:顺序表从原理到实现「增删改查」
数据结构·算法
I_LPL2 小时前
day52 代码随想录算法训练营 图论专题6
java·数据结构·算法·图论
lxl13072 小时前
C++算法(11)字符串
开发语言·c++·算法