深入理解红黑树:从概念到完整C++实现详解

一、红黑树概述:优雅的自平衡结构

红黑树是一种自平衡的二叉搜索树,它通过在节点上增加一个颜色标记(红或黑)并遵循特定的规则,来确保树保持大致平衡的状态。相比于AVL树,红黑树对平衡的要求更加宽松,但在实际应用中性能同样出色。

红黑树的五大核心规则

  1. 颜色规则:每个节点不是红色就是黑色
  2. 根规则:根节点必须是黑色
  3. 红色节点规则:红色节点的子节点必须是黑色(不能有连续的红色节点)
  4. 黑色节点规则:从任意节点到其所有空叶子节点(NIL)的路径上,黑色节点的数量必须相同
  5. 叶子规则:所有叶子节点(NIL)都是黑色(这一条常被隐含处理)

红黑树为什么能保证平衡?

红黑树的精妙之处在于它通过颜色约束间接控制了树的平衡:

  • 最短路径:全是黑色节点(由规则4保证所有路径黑色节点数相同)
  • 最长路径:红黑交替(由规则3保证不会出现连续红色节点)

假设最短路径长度为 bh(black height),那么:

  • 最短路径长度 = bh
  • 最长路径长度 ≤ 2 × bh

因此,最长路径不会超过最短路径的两倍,这保证了树的近似平衡。

红黑树 vs AVL树

特性 红黑树 AVL树
平衡标准 近似平衡 严格平衡
旋转频率 较少 较多
查找效率 O(log N) O(log N)
插入/删除 旋转少,性能好 旋转多,性能稍差
适用场景 需要频繁插入删除 查找密集,插入删除少

二、红黑树的结构设计

节点结构

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

// 红黑树节点
template<class K, class V>
struct RBTreeNode {
    std::pair<K, V> _kv;            // 键值对
    RBTreeNode<K, V>* _left;        // 左孩子
    RBTreeNode<K, V>* _right;       // 右孩子
    RBTreeNode<K, V>* _parent;      // 父节点(重要!用于回溯)
    Colour _col;                    // 颜色
    
    RBTreeNode(const std::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:
    RBTree() : _root(nullptr) {}
    
    // 基本操作
    bool Insert(const std::pair<K, V>& kv);
    Node* Find(const K& key);
    bool IsBalance();  // 验证红黑树性质
    
    // 旋转操作(与AVL树类似)
    void RotateL(Node* parent);   // 左旋
    void RotateR(Node* parent);   // 右旋
    
private:
    Node* _root = nullptr;
};

三、红黑树的插入操作详解

红黑树的插入分为两大步:BST标准插入 + 颜色调整

3.1 插入步骤概览

  1. 按BST规则插入:找到合适位置插入新节点
  2. 颜色处理
    • 空树插入:新节点设为黑色
    • 非空树插入:新节点设为红色(如果设为黑色会破坏规则4)
  3. 调整平衡
    • 如果父节点是黑色:插入完成
    • 如果父节点是红色:需要调整(违反规则3)

3.2 关键概念和符号

在讨论调整策略前,先定义几个重要节点:

  • c (cur):当前节点(新插入的节点)
  • p (parent):c的父节点
  • g (grandfather):p的父节点
  • u (uncle):p的兄弟节点

3.3 插入调整的三种情况

情况一:叔叔节点u存在且为红色

处理方式变色即可,不需要旋转

cpp 复制代码
// p和u变黑,g变红
p->_col = BLACK;
u->_col = BLACK;
g->_col = RED;

// 将g作为新的c,继续向上调整
cur = grandfather;
parent = cur->_parent;

原理分析

  • p和u变黑:左右子树各增加一个黑节点
  • g变红:保持子树黑色节点总数不变
  • 继续向上处理:因为g变红后,可能与其父节点形成连续红色
情况二:叔叔节点u不存在或为黑色,且c是p的左孩子

处理方式右单旋 + 变色

cpp 复制代码
// 以g为轴右旋
RotateR(g);

// 变色
p->_col = BLACK;
g->_col = RED;

图形表示

复制代码
      g(B)                  p(B)
     /   \                 /   \
    p(R)  u(B)   ==>     c(R)  g(R)
   /                           \
  c(R)                         u(B)
情况三:叔叔节点u不存在或为黑色,且c是p的右孩子

处理方式左右双旋 + 变色

cpp 复制代码
// 先以p为轴左旋,再以g为轴右旋
RotateL(p);
RotateR(g);

// 变色
c->_col = BLACK;
g->_col = RED;

图形表示

复制代码
      g(B)                  c(B)
     /   \                 /   \
    p(R)  u(B)   ==>     p(R)  g(R)
     \                         /
      c(R)                   u(B)

3.4 完整插入代码实现

cpp 复制代码
bool Insert(const std::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.1:叔叔存在且为红
            if (uncle && uncle->_col == RED) {
                // 变色处理
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandfather->_col = RED;
                
                // 继续向上调整
                cur = grandfather;
                parent = cur->_parent;
            } 
            // 情况1.2:叔叔不存在或为黑
            else {
                // 情况1.2.1:c是p的左孩子(右单旋)
                if (cur == parent->_left) {
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } 
                // 情况1.2.2:c是p的右孩子(左右双旋)
                else {
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;  // 旋转后调整完成
            }
        } 
        // 情况二:父节点是祖父的右孩子(对称处理)
        else {
            Node* uncle = grandfather->_left;
            
            // 情况2.1:叔叔存在且为红
            if (uncle && uncle->_col == RED) {
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandfather->_col = RED;
                
                cur = grandfather;
                parent = cur->_parent;
            } 
            // 情况2.2:叔叔不存在或为黑
            else {
                // 情况2.2.1:c是p的右孩子(左单旋)
                if (cur == parent->_right) {
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } 
                // 情况2.2.2:c是p的左孩子(右左双旋)
                else {
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;  // 旋转后调整完成
            }
        }
    }
    
    // 保证根节点为黑色
    _root->_col = BLACK;
    return true;
}

四、旋转操作实现

红黑树的旋转与AVL树类似,但不需要更新平衡因子:

cpp 复制代码
// 左旋(以parent为旋转点)
void RotateL(Node* parent) {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    
    // 1. subRL成为parent的右孩子
    parent->_right = subRL;
    if (subRL) {
        subRL->_parent = parent;
    }
    
    // 2. 记录parent的父节点
    Node* parentParent = parent->_parent;
    
    // 3. parent成为subR的左孩子
    subR->_left = parent;
    parent->_parent = subR;
    
    // 4. subR连接到原parent的父节点
    if (parentParent == nullptr) {
        _root = subR;
        subR->_parent = nullptr;
    } else {
        if (parent == parentParent->_left) {
            parentParent->_left = subR;
        } else {
            parentParent->_right = subR;
        }
        subR->_parent = parentParent;
    }
}

// 右旋(对称操作)
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;
    }
}

五、红黑树的验证

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

cpp 复制代码
// 检查红黑树性质
bool IsBalance() {
    // 空树是红黑树
    if (_root == nullptr) {
        return true;
    }
    
    // 规则2:根节点必须是黑色
    if (_root->_col != BLACK) {
        std::cout << "违反规则2:根节点不是黑色" << std::endl;
        return false;
    }
    
    // 计算参考值:任意一条路径的黑色节点数
    int refBlackNum = 0;
    Node* cur = _root;
    while (cur) {
        if (cur->_col == BLACK) {
            refBlackNum++;
        }
        cur = cur->_left;  // 走最左路径
    }
    
    // 递归检查所有规则
    return Check(_root, 0, refBlackNum);
}

// 递归检查辅助函数
bool Check(Node* root, int blackNum, int refBlackNum) {
    if (root == nullptr) {
        // 到达叶子,检查黑色节点数
        if (blackNum != refBlackNum) {
            std::cout << "违反规则4:路径黑色节点数不一致" << std::endl;
            return false;
        }
        return true;
    }
    
    // 规则3:检查连续红色节点(通过检查父节点)
    if (root->_col == RED && root->_parent && root->_parent->_col == RED) {
        std::cout << "违反规则3:存在连续红色节点" << std::endl;
        return false;
    }
    
    // 统计黑色节点数
    if (root->_col == BLACK) {
        blackNum++;
    }
    
    // 递归检查左右子树
    return Check(root->_left, blackNum, refBlackNum) &&
           Check(root->_right, blackNum, refBlackNum);
}

六、查找操作

红黑树的查找与普通BST相同:

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;  // 未找到
}

七、测试用例

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>

void TestRBTree() {
    std::cout << "=== 红黑树测试 ===" << std::endl;
    
    RBTree<int, int> tree;
    
    // 测试用例1:插入测试
    std::vector<int> nums = {16, 3, 7, 11, 9, 26, 18, 14, 15};
    
    std::cout << "插入序列: ";
    for (int num : nums) {
        std::cout << num << " ";
        tree.Insert({num, num * 10});
    }
    std::cout << std::endl;
    
    std::cout << "红黑树是否平衡: " << tree.IsBalance() << std::endl;
    
    // 测试用例2:查找测试
    std::cout << "\n查找测试:" << std::endl;
    for (int num : {7, 15, 100}) {
        auto node = tree.Find(num);
        if (node) {
            std::cout << "找到键 " << num << ", 值 = " << node->_kv.second << std::endl;
        } else {
            std::cout << "未找到键 " << num << std::endl;
        }
    }
    
    // 测试用例3:大量数据测试
    std::cout << "\n大量数据测试:" << std::endl;
    RBTree<int, int> largeTree;
    std::srand(std::time(0));
    
    const int N = 10000;
    for (int i = 0; i < N; i++) {
        int key = std::rand() % 100000;
        largeTree.Insert({key, i});
    }
    
    std::cout << "插入" << N << "个随机键后,树是否平衡: " 
              << largeTree.IsBalance() << std::endl;
}

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

八、总结

红黑树的核心思想

  1. 颜色标记:通过红黑颜色实现简单的状态表示
  2. 规则约束:四条简单规则保证树的平衡性
  3. 局部调整:插入/删除时只影响局部,通过变色和旋转修复

红黑树的优势

  1. 性能稳定:最坏情况也是O(log N)
  2. 维护成本低:比AVL树旋转次数少
  3. 内存友好:每个节点只多一个颜色位
  4. 应用广泛:C++ STL的map/set、Linux内核、Java的TreeMap等都使用红黑树

学习要点

  1. 理解规则:四条规则必须牢记
  2. 掌握情况分析:三种插入情况的处理方式
  3. 练习实现:手动实现加深理解
  4. 对比学习:与AVL树对比,理解不同平衡策略

红黑树是数据结构中的经典内容,虽然实现较为复杂,但理解其原理后,你会发现它的设计非常精妙,是平衡性能和实现复杂度的绝佳折中方案。

相关推荐
每天学习一丢丢1 小时前
Spring Boot 调用泛微 E9 Token 认证 + 创建流程完整教程
java·spring boot·后端
Dave.B1 小时前
:vtkBooleanOperationPolyDataFilter 布尔运算全解析
算法·vtk
楼田莉子1 小时前
CMake学习:入门及其下载配置
开发语言·c++·vscode·后端·学习
苦逼的老王2 小时前
《java-使用kkview+libreoffice 实现在线预览ppt、xls、doc、pdf..》
java·pdf·powerpoint
易晨 微盛·企微管家2 小时前
2025企业微信AI智能机器人实战指南:3步实现客服自动化
大数据·人工智能·算法
没有bug.的程序员2 小时前
Spring Boot 启动原理:从 @SpringBootApplication 到自动配置深度解析
java·spring boot·后端·python·springboot·application
jiaguangqingpanda2 小时前
Day26-20260122
java·算法·排序算法
Ahtacca2 小时前
拒绝重复造轮子:利用自定义注解封装POI,实现Java通用Excel解析
java·javascript·vue·excel
夜月yeyue2 小时前
VFS (虚拟文件系统) 核心架构
linux·c++·单片机·嵌入式硬件·架构