目录

引言

一、红黑树的概念与平衡原理

1.1 红黑树的定义

1.2 红黑树五条规则

1.3 平衡原理

1.4 红黑树的效率

二、红黑树与 AVL 树的对比

三、红黑树的实现

3.1 节点结构与定义

3.2 旋转实现

3.3 插入实现

3.4 查找实现

3.5 时间复杂度

3.6 空间复杂度

四、红黑树验证

五、红黑树的展与建议

5.1 红黑树的应用场景

5.2 平衡树家族对比

5.3 红黑树的优化点

六、总结


引言

在数据结构领域,平衡二叉搜索树(BBST)是解决 “二叉搜索树退化为链表” 问题的核心方案。其中,红黑树凭借近似平衡的特性、更少的旋转次数和稳定的 O (logN) 时间复杂度,成为工业界应用最广泛的平衡树(例如 C++ STL 的map/set、Linux 内核的 epoll、Java 的TreeMap等底层均采用红黑树)。

一、红黑树的概念与平衡原理

1.1 红黑树的定义

红黑树是一棵二叉搜索树,它的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。

1.2 红黑树五条规则

规则编号 规则描述 补充说明
1 每个节点非红即黑 颜色是平衡控制的核心标识
2 根节点必须为黑色 全局平衡的基准约束
3 红色节点的两个子节点必须为黑色 禁止连续红色节点,避免路径过长
4 任意节点到其所有 NIL 节点(空节点)的路径,黑色节点数量相等 保证 “黑高一致”,限制路径差异

⚠️ 关键补充:《算法导论》中明确 “NIL 节点(外部节点)为黑色”,其作用是统一路径的终点判断(避免叶子节点无孩子导致规则 4 验证复杂),实际实现中可简化为nullptr,但逻辑上需视为黑色空节点。

1.3 平衡原理

思考一下:为什么最长路径不超过最短路径的 2 倍?

红黑树的 “近似平衡” 源于规则 3 和规则 4 的协同约束,我们通过数学推导证明核心结论:设任意路径的黑高为bh(从节点到 NIL 的黑色节点数,根节点的黑高为树的黑高),路径总长度为h,则bh ≤ h ≤ 2bh

推导过程:

  1. 最短路径:全由黑色节点组成(无红色节点插入,路径长度 = 黑高bh);

  2. 最长路径:红黑节点交替出现(规则 3 禁止连续红色,因此最长路径是 “黑 - 红” 交替,长度 = 2bh);

  3. 结论:最长路径长度 ≤ 2× 最短路径长度,确保树不会过度失衡。

  • 由规则4可知,从根到NULL结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径就是全是黑色结点的路径,假设最短路径长度为bh(black height)。

  • 由规则2和规则3可知,任意一条路径不会有连续的红色结点,所以极端场景下,最长的路径就是一一黑一红间隔组成,那么最长路径的长度为2*bh。 

  • 综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都 存在的。假设任意一条从根到NULL结点路径的长度为x,那么bh <= h <= 2*bh。

1.4 红黑树的效率

红黑树的时间复杂度由树的高度h决定,结合二叉搜索树的节点数与高度关系:

  • 设树的黑高为bh,则最短路径节点数满足:N ≥ 2^bh - 1(全黑路径的完全二叉树);

  • 最长路径节点数满足:N ≤ 2^(2bh) - 1(红黑交替的完全二叉树);

  • 联立推导得:bh ≈ logN,因此树高h ≤ 2logN,最终所有操作(增删查改)的时间复杂度为O(logN)

二、红黑树与 AVL 树的对比

AVL 树是 “严格平衡” 二叉树(左右子树高度差≤1),与红黑树的核心差异体现在 “平衡控制粒度”,以下是工程选型的关键对比数据:

对比维度 红黑树 AVL 树
平衡标准 近似平衡(黑高一致) 严格平衡(高度差≤1)
旋转次数 插入最多 2 次旋转,删除最多 3 次 插入 / 删除最多 2 次旋转,但触发频率更高
空间开销 仅需 1 个颜色位(枚举) 需存储平衡因子(int,4 字节)
适用场景 频繁插入 / 删除(旋转少) 频繁查询(严格平衡,路径更短)
工业应用 STL、Linux 内核、Java 集合 数据库索引(部分场景)

可视化对比:插入 10 万节点的旋转次数统计

通过模拟插入随机数据,统计两种树的旋转次数:

结论:红黑树的旋转次数约为 AVL 树的 65%-70%,插入效率更优,适合写密集场景。

三、红黑树的实现

红黑树的实现核心是插入后的平衡调整(删除逻辑复杂,本文聚焦插入与查找,删除可参考《STL 源码剖析》),实现分为 3 个模块:节点结构定义、旋转操作、插入逻辑与平衡调整、验证逻辑。

3.1 节点结构与定义

#include <iostream>
#include <utility>
using namespace std;

// 颜色枚举
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) {} // 新增节点默认红色(关键优化:避免破坏规则4)
};

// 红黑树类模板
template<class K, class V>
class RBTree {
    typedef RBTreeNode<K, V> Node;
public:
    RBTree() : _root(nullptr) {}

    // 插入接口
    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; // 根节点
};

3.2 旋转实现

旋转是平衡树调整的基础,目的是 “改变子树结构但不破坏二叉搜索树性质”,红黑树的旋转与 AVL 树完全一致(仅无需更新平衡因子):

// 右单旋:parent为旋转中心,左孩子child上位
template<class K, class V>
void RBTree<K, V>::RotateR(Node* parent) {
    Node* child = parent->_left;
    Node* childRight = child->_right;

    // 1. 处理childRight的父指针
    if (childRight) childRight->_parent = parent;
    parent->_left = childRight;

    // 2. 处理child与parent父节点的关系
    Node* grandParent = parent->_parent;
    child->_parent = grandParent;
    if (grandParent == nullptr) {
        _root = child; // child成为新根
    } else if (grandParent->_left == parent) {
        grandParent->_left = child;
    } else {
        grandParent->_right = child;
    }

    // 3. 处理child与parent的关系
    child->_right = parent;
    parent->_parent = child;
}

// 左单旋:parent为旋转中心,右孩子child上位(对称实现)
template<class K, class V>
void RBTree<K, V>::RotateL(Node* parent) {
    Node* child = parent->_right;
    Node* childLeft = child->_left;

    if (childLeft) childLeft->_parent = parent;
    parent->_right = childLeft;

    Node* grandParent = parent->_parent;
    child->_parent = grandParent;
    if (grandParent == nullptr) {
        _root = child;
    } else if (grandParent->_left == parent) {
        grandParent->_left = child;
    } else {
        grandParent->_right = child;
    }

    child->_left = parent;
    parent->_parent = child;
}

3.3 插入实现

  1. 插入一个值按二叉搜索树规则进行插入,插入后我们只需要观察是否符合红黑树的4条规则。 

  2. 如果是空树插入,新增结点是黑色结点。如果是非空树插入,新增结点必须红色结点,因为非空树插入,新增黑色结点就破坏了规则4,规则4是很难维护的。 

  3. 非空树插入后,新增结点必须红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束 

  4. 非空树插入后,新增结点必须红色结点,如果父亲结点是红色的,则违反规则3。进一步分析,c是 红色,p为红,g必为黑,这三个颜色都固定了,关键的变化看u的情况,需要根据u分为以下几种 情况分别处理。 

说明:下图中假设我们把新增结点标识为c (cur),c的父亲标识为p(parent),p的父亲标识为 g(grandfather),p的兄弟标识为u(uncle)。

调整的核心是根据 “叔叔节点(父节点的兄弟)的颜色” 分为 3 种场景:

场景 1:叔叔存在且为红色(仅变色,不旋转)

  • 条件cur(新增节点,红)→ parent(红)→ grandParent(黑)→ uncle(红)

  • 操作parent变黑 + uncle变黑 + grandParent变红,然后将grandParent作为新cur向上递归检查(可能触发更高层调整)。

  • 原理:保持各路径黑高不变,同时消除 “连续红色节点”。

图0
  • 图1将以上类似的处理进行了抽象表达,d/e/f代表每条路径拥有hb个黑色结点的子树,a/b代表每 条路径拥有hb-1个黑色结点的根为红的子树,hb>=0。 

  • 图2/图3/图4,分别展示了hb == 0/hb == 1/hb == 2的具体情况组合分析,当hb等于2时,这里组合 

  • 情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理方式一样的,变色再 继续往上处理即可,所以我们只需要看抽象图即可。

图1
图2
图3
图4

场景 2:叔叔不存在或为黑色(单旋 + 变色)

  • 条件cur是parent的左孩子,parent是grandParent的左孩子(或对称的右右结构)

  • 操作以grandParent为中心右单旋(或左单旋)→ parent变黑 → grandParent变红

  • 原理:旋转后调整颜色,既保持二叉搜索树性质,又修复连续红节点和黑高平衡。

场景 3:叔叔不存在或为黑色(双旋 + 变色)

  • 条件cur是parent的右孩子,parent是grandParent的左孩子(或对称的左右结构)

  • 操作:先以parent为中心左单旋 → 转化为场景2 → 再以grandParent为中心右单旋 → cur变黑 → grandParent变红

  • 原理:通过双旋将 “非对称结构” 转化为 “对称结构”,再复用场景 2 的逻辑。

红黑树插入代码完整实现

template<class K, class V>
bool RBTree<K, V>::Insert(const pair<K, V>& kv) {
    // 第一步:空树处理
    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; // 键值重复,插入失败
        }
    }

    // 新增节点(默认红色)
    cur = new Node(kv);
    if (parent->_kv.first < kv.first) {
        parent->_right = cur;
    } else {
        parent->_left = cur;
    }
    cur->_parent = parent;

    // 第三步:平衡调整(仅当父节点为红色时需要)
    while (parent && parent->_col == RED) {
        Node* grandParent = parent->_parent; // 祖父节点必为黑(规则3:父红→祖父不能红)

        // 情况A:父节点是祖父的左孩子
        if (parent == grandParent->_left) {
            Node* uncle = grandParent->_right; // 叔叔节点

            // 场景1:叔叔存在且为红色 → 变色
            if (uncle && uncle->_col == RED) {
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandParent->_col = RED;

                // 向上递归检查
                cur = grandParent;
                parent = cur->_parent;
            } else { // 场景2/3:叔叔不存在或为黑色 → 旋转+变色
                // 场景3:cur是parent的右孩子 → 先左旋转化为场景2
                if (cur == parent->_right) {
                    RotateL(parent);
                    swap(parent, cur); // 转化后cur成为parent的左孩子,复用场景2逻辑
                }

                // 场景2:cur是parent的左孩子 → 右旋+变色
                RotateR(grandParent);
                parent->_col = BLACK;
                grandParent->_col = RED;
                break; // 调整完成,无需向上递归
            }
        } else { // 情况B:父节点是祖父的右孩子(对称逻辑)
            Node* uncle = grandParent->_left;

            if (uncle && uncle->_col == RED) {
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandParent->_col = RED;

                cur = grandParent;
                parent = cur->_parent;
            } else {
                if (cur == parent->_left) {
                    RotateR(parent);
                    swap(parent, cur);
                }

                RotateL(grandParent);
                parent->_col = BLACK;
                grandParent->_col = RED;
                break;
            }
        }
    }

    // 确保根节点始终为黑色(防止grandParent是根且被染红)
    _root->_col = BLACK;
    return true;
}

3.4 查找实现

按二叉搜索树逻辑实现即可,搜索效率为 O(logN)

template<class K, class V>
RBTreeNode<K, V>* 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; // 未找到
}

3.5 时间复杂度

操作 平均时间复杂度 最坏时间复杂度
插入 O(logN) O(logN)
删除 O(logN) O(logN)
查找 O(logN) O(logN)
更新 O(logN) O(logN)

3.6 空间复杂度

  • 节点存储:O(N)

  • 递归栈深度:O(logN)(验证操作)

四、红黑树验证

红黑树验证需检查所有五条规则:

  1. 规则1:颜色枚举保证

  2. 规则2:检查根节点颜色

  3. 规则3:检查红色节点的子节点是否为黑色

  4. 规则4:检查所有路径黑色节点数量相等

  5. 规则5:NIL节点颜色(实际实现中常忽略)

template<class K, class V>
bool RBTree<K, V>::Check(Node* root, int blackNum, const int refNum) {
    // 递归到NIL节点,检查当前路径黑高是否与参考黑高一致
    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);
}

五、红黑树的展与建议

5.1 红黑树的应用场景

应用场景 选择红黑树的原因
C++ STL map/set 插入删除频繁,需要稳定的 O (logN) 复杂度
Linux 内核 epoll 事件节点的快速插入、删除与查找
Java TreeMap/TreeSet 无锁场景下的有序集合,旋转少效率高
数据库索引(部分场景) 内存中有序数据的高效管理(磁盘索引更常用 B + 树)

5.2 平衡树家族对比

除了红黑树和 AVL 树,平衡树家族还包括 B 树、B + 树、Splay 树等,核心选型逻辑如下:

5.3 红黑树的优化点

  1. NIL 节点优化:实际实现中可使用一个全局共享的 NIL 节点,减少空指针判断,简化规则 4 验证;

  2. 迭代式调整:将插入后的递归调整改为迭代,避免栈溢出;

  3. 键值分离:对于大 value 场景,可采用 “键 + 指针” 存储,减少节点体积,提升缓存命中率。

六、总结

红黑树作为一种高效的自平衡二叉搜索树,通过巧妙的颜色约束规则实现了近似平衡。相比于严格的AVL树,红黑树在插入删除操作上具有更好的平均性能,同时保持了O(logN)的时间复杂度。

关键要点

  1. 理解红黑树的五条核心规则及其平衡原理

  2. 掌握插入操作中的三种情况处理(变色、单旋、双旋)

  3. 熟悉红黑树的验证方法,确保实现正确性

  4. 了解红黑树与AVL树的适用场景差异

红黑树的实现虽然有一定复杂度,但其在实际系统中的应用价值使其成为每个高级开发者应当掌握的数据结构之一。

附录:完整测试代码

// 测试代码
int main() {
    RBTree<int, int> rbt;
    int arr[] = {18, 10, 30, 6, 15, 25, 40, 1, 8, 12, 16, 20, 22, 35, 50};
    for (auto& num : arr) {
        rbt.Insert({num, num});
    }

    // 验证平衡
    if (rbt.IsBalance()) {
        cout << "红黑树插入完成,平衡验证通过!" << endl;
    } else {
        cout << "红黑树平衡验证失败!" << endl;
    }

    // 查找测试
    Node* findNode = rbt.Find(25);
    if (findNode) {
        cout << "找到节点:" << findNode->_kv.first << ",颜色:" << (findNode->_col == RED ? "红色" : "黑色") << endl;
    }

    return 0;
}

参考资料

  1. 《算法导论》第三版 - Thomas H. Cormen

  2. 《STL源码剖析》 - 侯捷

  3. Linux内核源码中红黑树实现

  4. C++ Standard Template Library Documentation

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐