【C++】红黑树完全解析:从概念到插入与平衡维护

引言

红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在每个节点上增加一个存储位表示颜色(红色或黑色),通过对根到叶子路径上节点颜色的约束,确保没有一条路径会比其他路径长出两倍,从而保持树的近似平衡。相比于 AVL 树要求严格的高度差不超过 1,红黑树对平衡的控制更为宽松,因此插入和删除时的旋转次数更少,整体性能在实际应用中往往更优。C++ 标准库中的 std::mapstd::set 以及 Linux 内核的调度器、Java 的 HashMap(红黑树部分)等大量场景都采用了红黑树作为底层数据结构。

本文将从红黑树的核心规则出发,详细阐述其如何保证最长路径不超过最短路径的 2 倍,分析其时间复杂度,然后系统讲解红黑树的节点结构、插入算法的完整流程(包括变色、单旋+变色、双旋+变色三种情况),并给出平衡验证方法。


目录

引言

一、红黑树的概念与规则

[1.1 红黑树的四大约束](#1.1 红黑树的四大约束)

[1.2 红黑树如何保证平衡](#1.2 红黑树如何保证平衡)

[1.3 红黑树的效率](#1.3 红黑树的效率)

二、红黑树的节点结构

三、红黑树的插入

[3.1 插入的宏观流程](#3.1 插入的宏观流程)

[3.2 情况一:叔叔存在且为红色(变色)](#3.2 情况一:叔叔存在且为红色(变色))

[3.3 情况二:叔叔不存在或为黑色(单旋+变色)](#3.3 情况二:叔叔不存在或为黑色(单旋+变色))

[3.4 情况三:叔叔不存在或为黑色(双旋+变色)](#3.4 情况三:叔叔不存在或为黑色(双旋+变色))

[3.5 旋转代码](#3.5 旋转代码)

[3.6 插入完整代码框架](#3.6 插入完整代码框架)

四、红黑树的查找

五、红黑树的验证

六、关于删除的说明

总结


一、红黑树的概念与规则

1.1 红黑树的四大约束

红黑树本质上是一棵二叉搜索树,每个节点额外存储一个颜色属性,且必须满足以下四条规则:

  1. 节点颜色:每个节点不是红色就是黑色。

  2. 根节点:根节点必须是黑色。

  3. 红色节点的子节点:如果一个节点是红色的,那么它的两个孩子节点都必须是黑色的(即任何路径上不允许出现连续的红色节点)。

  4. 黑色节点数量相同:对于任意一个节点,从该节点出发到其所有后代空节点(NULL,视为黑色)的简单路径上,所经过的黑色节点数量必须相同。

注:《算法导论》中明确将 NULL 节点视为黑色叶子节点(称为 NIL 外部节点),这有助于统一路径计数。但在具体实现中,通常只需在代码逻辑中体现这一规则即可。

1.2 红黑树如何保证平衡

由规则 4 可知,从根到任一空节点的路径上黑色节点数量相同,记这个黑色节点数为 bh(black height)。由规则 2 和规则 3 可知,红色节点不能连续出现,因此最长路径必然由"黑‑红‑黑‑红‑..."交替构成,其长度最多为 2 * bh;而最短路径则可能全部由黑色节点组成,长度为 bh。因此,最长路径不会超过最短路径的 2 倍

需要注意的是,实际的红黑树不一定同时存在"全黑最短路径"和"一黑一红最长路径",但上述理论界保证了树的高度始终维持在 O(\\log N)

1.3 红黑树的效率

设红黑树中节点总数为 N,树高为 h。由于最短路径长度至少为 \\log_2(N+1)(满二叉树情形),最长路径至多为 2\\log_2(N+1),因此 h \\le 2\\log_2(N+1),即 h = O(\\log N)。所以红黑树的查找、插入、删除操作的时间复杂度均为 O(\\log N)

与 AVL 树相比,红黑树对平衡的约束较为宽松,因而在插入和删除时所需的旋转次数更少,整体常数因子更小。实际工程中,红黑树的应用更为广泛。


二、红黑树的节点结构

红黑树节点除了存储键值对(pair<K,V>)和左右孩子指针外,还需要存储父节点指针(便于向上回溯)以及颜色枚举值。

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)   // 新节点默认为红色
    {}
};

树的封装类仅需维护根节点指针:

cpp

复制代码
template<class K, class V>
class RBTree {
    typedef RBTreeNode<K, V> Node;
public:
    // 插入、查找、验证等接口
private:
    Node* _root = nullptr;
};

三、红黑树的插入

红黑树的插入分为三个主要步骤:

  1. 按二叉搜索树的规则插入新节点。

  2. 将新节点颜色设为红色(若插入后为根节点则设为黑色)。

  3. 检查是否违反红黑树规则,若违反则通过变色旋转进行调整。

3.1 插入的宏观流程

  • 空树:直接插入黑色根节点。

  • 非空树:插入红色新节点。若父节点为黑色,则无需调整;若父节点为红色,则违反规则 3(连续红色),需要处理。

设当前新节点为 c(cur),父节点 p(parent),祖父节点 g(grandfather),叔叔节点 u(uncle)。由于 p 为红色,而规则 2 要求根为黑色,因此 g 必然存在且为黑色。调整的关键在于 u 的状态(存在且为红色,或不存在/存在且为黑色)。

3.2 情况一:叔叔存在且为红色(变色)

特征u 存在且为红色。

处理 :将 pu 变为黑色,g 变为红色。此时以 g 为根的子树黑色节点数量保持不变(因为原本 pu 红色变黑,g 黑色变红,局部黑色节点数不变),但 g 变为红色后可能与其父节点形成连续红色。因此将 g 视为新的 c,继续向上检查。

特点 :只变色,不旋转。无论 cp 的左还是右,pg 的左还是右,处理方式相同。

cpp

复制代码
// 伪代码示例
if (uncle && uncle->_col == RED) {
    parent->_col = BLACK;
    uncle->_col = BLACK;
    grandfather->_col = RED;
    cur = grandfather;
    parent = cur->_parent;
}

3.3 情况二:叔叔不存在或为黑色(单旋+变色)

特征u 不存在(即 p 为叶子)或 u 存在且为黑色。此时 cp 的连线方向一致(即 pg 的左且 cp 的左,或 pg 的右且 cp 的右)。

处理

  • pg 的左,cp 的左:对 g 进行右单旋

  • pg 的右,cp 的右:对 g 进行左单旋

旋转后,将 p 变为黑色,g 变为红色。此时 p 成为新的子树根,子树黑色节点数不变且无连续红色,且 p 的父亲(原 g 的父亲)可能是任意颜色,但由于 p 为黑色,不会破坏规则,因此调整结束。

代码示例(以右单旋为例):

cpp

复制代码
if (cur == parent->_left) {
    RotateR(grandfather);
    parent->_col = BLACK;
    grandfather->_col = RED;
}

3.4 情况三:叔叔不存在或为黑色(双旋+变色)

特征u 不存在或为黑色,且 cp 的连线方向不一致(即 pg 的左,cp 的右;或 pg 的右,cp 的左)。

处理

  • pg 的左,cp 的右:先对 p 左单旋,再对 g 右单旋。

  • pg 的右,cp 的左:先对 p 右单旋,再对 g 左单旋。

旋转后,将 c 变为黑色,g 变为红色。此时 c 成为新的子树根,同样保证黑色节点数不变且无连续红色,调整结束。

代码示例(左右双旋):

cpp

复制代码
if (cur == parent->_right) {
    RotateL(parent);
    RotateR(grandfather);
    cur->_col = BLACK;
    grandfather->_col = RED;
}

3.5 旋转代码

红黑树的旋转与 AVL 树在结构上完全一致,区别在于不需要更新平衡因子,仅需调整父子指针和颜色。以下以右单旋为例(左单旋对称):

cpp

复制代码
void RotateR(Node* parent) {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;

    Node* parentParent = parent->_parent;
    subL->_right = parent;
    parent->_parent = subL;

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

3.6 插入完整代码框架

cpp

复制代码
bool Insert(const pair<K, V>& kv) {
    // 1. 空树处理
    if (_root == nullptr) {
        _root = new Node(kv);
        _root->_col = BLACK;
        return true;
    }

    // 2. BST 插入
    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;
        }
    }
    cur = new Node(kv);
    cur->_col = RED;
    if (parent->_kv.first < kv.first)
        parent->_right = cur;
    else
        parent->_left = cur;
    cur->_parent = parent;

    // 3. 调整颜色与旋转
    while (parent && parent->_col == RED) {
        Node* grandfather = parent->_parent;
        if (parent == grandfather->_left) {
            Node* uncle = grandfather->_right;
            // 情况1:叔叔存在且红
            if (uncle && uncle->_col == RED) {
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            } else {
                // 情况2/3:叔叔不存在或黑
                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 == grandfather->_right
            Node* uncle = grandfather->_left;
            if (uncle && uncle->_col == RED) {
                parent->_col = BLACK;
                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;
            }
        }
    }
    _root->_col = BLACK;
    return true;
}

四、红黑树的查找

查找操作与普通二叉搜索树完全一致,时间复杂度 O(\\log N)

cpp

复制代码
Node* 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;
}

五、红黑树的验证

验证一棵树是否为红黑树,必须检查其是否满足全部四条规则:

  1. 根节点为黑色 :直接判断 _root->_col

  2. 无连续红色节点:前序遍历时检查每个红色节点的父节点是否也为红色。

  3. 每条路径黑色节点数相同 :先选取一条路径(如最左路径)的黑色节点数作为参考值 refNum,然后递归检查每条路径的黑色节点数是否等于 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 << "连续红色节点" << 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);
}

注意:不能简单地通过"最长路径不超过最短路径的 2 倍"来验证,因为该条件满足时仍可能违反颜色规则(例如红色节点连续但路径长度仍满足 2 倍关系)。


六、关于删除的说明

红黑树的删除操作比插入更为复杂,需要在删除节点后通过变色和旋转恢复红黑树性质。由于篇幅和常见教学安排,本文不展开删除的具体实现,有兴趣的读者可参考《算法导论》或《STL 源码剖析》。


总结

红黑树通过四类颜色约束实现了近似平衡,确保了任何一条路径的长度不超过最短路径的两倍,从而保证查找、插入、删除操作的时间复杂度均为 O(\\log N)。其插入算法以二叉搜索树插入为基础,将新节点设为红色,然后根据叔叔节点的颜色分为三种情形:

  • 叔叔为红:仅通过变色处理,并向上传递;

  • 叔叔为黑或不存在,且新节点与父节点方向一致:单旋 + 变色;

  • 叔叔为黑或不存在,且新节点与父节点方向不一致:双旋 + 变色。

每次旋转和变色都维持了红黑树的性质,且调整后局部子树高度不变,因此无需继续向上更新。

相比于 AVL 树,红黑树牺牲了部分平衡的严格性,换来了更少的旋转次数和更简单的维护逻辑,因此在工业级代码中应用更为广泛。掌握红黑树的核心规则与插入调整策略,是深入理解高效关联容器(如 std::mapstd::set)内部实现的关键一步。

相关推荐
Liangwei Lin4 分钟前
LeetCode 155. 最小栈
java·javascript·算法
我叫黑大帅7 分钟前
PyScript-GitHubRepo: 构建高性能GitHub仓库批量下载工具的技术实践
后端·python·面试
洛水水9 分钟前
【数据结构】红黑树详解
数据结构·红黑树
炸膛坦客9 分钟前
嵌入式 - 数据结构与算法:(1-9)数据结构 - 队列(Queue)
c语言·数据结构
lbb 小魔仙20 分钟前
基于Python构建RAG(检索增强生成)系统:从原理到企业级实战
开发语言·python
~|Bernard|37 分钟前
二.go语言中map的底层原理(2026-5-8)
算法·golang·哈希算法
代码的小搬运工41 分钟前
UITableView
开发语言·ui·ios·objective-c
刚子编程44 分钟前
C# Join 深度解析:参数顺序、多表关联与空值处理最佳实践
开发语言·c#·最佳实践·join·多表关联·空值处理
mzhan01744 分钟前
Linux: compare的直观性
java·linux·服务器
AbandonForce1 小时前
哈希表(HashTable,散列表)个人理解
开发语言·数据结构·c++·散列表