代码改变世界

C++进阶:(七)红黑树深度解析与 C++ 建立

2025-12-03 17:07  tlnshuju  阅读(0)  评论(0)    收藏  举报

目录

前言

一、红黑树的核心概念

1.1 红黑树的定义

1.2 红黑树的五大规则

1.3 红黑树的平衡原理

1.4 红黑树的效率分析

二、红黑树的结构设计

2.1 结点结构定义

2.2 红黑树类的框架

2.3 旋转操作实现

2.3.1 右单旋(RotateR)

2.3.2 左单旋(RotateL)

三、红黑树的插入实现

3.1 插入的基础流程

3.2 平衡调整的核心场景

场景 1:叔叔结点存在且为红色(变色调整)

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

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

3.3 完整插入代码实现

3.4 插入代码关键要点

四、红黑树的查找与验证实现

4.1 查找操作实现

4.2 红黑树的验证实现

4.2.1 验证辅助函数(Check)

4.2.2 验证主函数(IsBalance)

五、红黑树的应用与扩展

5.1 红黑树的工业应用

5.2 红黑树的扩展(删除操作)

总结


前言

        在数据结构与算法领域,平衡二叉搜索树是解决高效查找、插入、删除操作的核心数据结构。AVL 树作为经典的平衡二叉搜索树,通过严格控制左右子树的高度差不超过 1,保证了 O (logN) 的时间复杂度,但频繁的旋转操作导致插入和删除效率偏低。红黑树则通过颜色约束实现 “近似平衡”,在保证 O (logN) 时间复杂度的前提下,大幅减少了旋转次数,成为工业界的首选 ——C++ STL 中的 set、map 等容器底层均采用红黑树实现。

        本文将从红黑树的核心概念出发,详细拆解其规则约束、平衡原理、插入逻辑、代码实现及验证方法。下面就让我们正式开始吧!


一、红黑树的核心概念

1.1 红黑树的定义

        红黑树是一棵二叉搜索树,在每个结点中增加一个存储位表示颜色(红色或黑色)。通过对从根到叶子的所有路径施加颜色约束,确保没有一条路径的长度超过其他路径的 2 倍,从而实现 “近似平衡”。

        二叉搜索树的基础特性:对于任意结点,其左子树中所有结点的关键字均小于该结点的关键字,右子树中所有结点的关键字均大于该结点的关键字。红黑树继承了这一特性,因此查找操作可直接复用二叉搜索树的逻辑。

1.2 红黑树的五大规则

        红黑树的平衡特性由以下五条规则严格约束(参考《算法导论》中的定义):

  1. 每个结点的颜色只能是红色或黑色
  2. 根结点必须是黑色
  3. 所有叶子结点(包括空结点)必须是黑色
  4. 如果一个结点是红色,那么它的两个子结点必须是黑色(无连续红色结点);
  5. 对于任意一个结点,从该结点到其所有后代 NIL 结点的简单路径上,包含的黑色结点数量相同(黑高一致)。

        说明

  • NIL 结点是逻辑上的空结点,用于统一路径处理逻辑,在实际实现中是可以省略的(通过 nullptr 标识),但需在思维上保留该概念;
  • 规则 3 和规则 5 是红黑树平衡的核心,规则 4 则是避免路径过长的关键约束。

1.3 红黑树的平衡原理

        红黑树通过规则约束,确保最长路径长度不超过最短路径长度的 2 倍,其推导过程如下:

  1. 定义 “黑高(bh)”:从某结点到其后代 NIL 结点的路径上,黑色结点的数量(不包含当前结点);
  2. 由规则 5 可知,所有根到 NIL 结点的路径黑高一致,设根结点的黑高为 bh;
  3. 最短路径:全由黑色结点组成(无红色结点),长度为 bh(路径上的结点数);
  4. 最长路径:红黑结点交替出现(由规则 4 限制,无连续红色结点),长度为 2*bh(红色结点数 = 黑色结点数);
  5. 由此可得:最短路径长度 ≤ 任意路径长度 ≤ 2 * 最短路径长度,即红黑树是 “近似平衡” 的。

1.4 红黑树的效率分析

        红黑树的时间复杂度由其高度决定,设树中结点数为 N,高度为 h:

  • 由二叉搜索树特性:N ≥ 2^bh - 1(全黑路径对应的满二叉树结点数);
  • 由最长路径约束:h ≤ 2*bh
  • 推导可得:h ≈ logN,因此查找、插入、删除操作的最坏时间复杂度均为 O (logN)

        与 AVL 树对比:

特性红黑树AVL 树
平衡条件颜色约束(近似平衡)高度差≤1(严格平衡)
旋转次数插入最多 2 次旋转插入最多 2 次旋转
删除次数删除最多 3 次旋转删除最多 logN 次旋转
适用场景频繁插入 / 删除的场景频繁查找的场景

        红黑树的优势在于对平衡的要求相对宽松,因此在插入和删除操作中需要的旋转次数更少,更加适合频繁修改数据的场景。

二、红黑树的结构设计

        红黑树的结点需要存储关键字、颜色、左右子指针及父指针(父指针用于向上回溯调整平衡),采用模板类设计以支持任意可比较类型的关键字。

2.1 结点结构定义

// 颜色枚举
enum Colour {
    RED,    // 红色结点
    BLACK   // 黑色结点
};
// 红黑树结点模板类
template
struct RBTreeNode {
    pair _kv;                  // 关键字-值对
    RBTreeNode* _left;         // 左子指针
    RBTreeNode* _right;        // 右子指针
    RBTreeNode* _parent;       // 父指针(用于平衡调整)
    Colour _col;                     // 结点颜色
    // 构造函数
    RBTreeNode(const pair& kv)
        : _kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED) {}               // 新增结点默认红色(关键设计)
};

        关键说明

  • 新增结点默认设为红色:若设为黑色,会直接破坏规则 5(黑高一致),而红色结点仅可能破坏规则 4(连续红色结点),后者更容易调整;
  • 父指针是红黑树平衡调整的核心:插入或删除后需要向上回溯父结点、祖父结点、叔叔结点的颜色和位置关系。

2.2 红黑树类的框架

        红黑树类包含根结点指针,以及插入、查找、验证等核心成员函数:

template
class RBTree {
    typedef RBTreeNode Node;   // 结点类型别名
public:
    // 构造函数
    RBTree() : _root(nullptr) {}
    // 插入操作
    bool Insert(const pair& kv);
    // 查找操作
    Node* Find(const K& key);
    // 验证红黑树合法性
    bool IsBalance();
private:
    // 右单旋(与AVL树旋转逻辑一致)
    void RotateR(Node* parent);
    // 左单旋(与AVL树旋转逻辑一致)
    void RotateL(Node* parent);
    // 递归验证红黑树规则
    bool Check(Node* root, int blackNum, const int refNum);
private:
    Node* _root;  // 根结点指针
};

2.3 旋转操作实现

        红黑树的旋转操作是与 AVL 树完全一致的,目的是调整子树结构以恢复平衡,无需修改结点颜色(颜色调整单独处理)。旋转操作分为右单旋和左单旋。

2.3.1 右单旋(RotateR)

        当左子树过高时,通过右单旋将左子树的根结点提升为新的父结点,原父结点变为新根的右子结点。

template
void RBTree::RotateR(Node* parent) {
    Node* subL = parent->_left;      // 左子树的根结点
    Node* subLR = subL->_right;      // 左子树的右孩子
    // 1. 处理subLR与parent的关系
    parent->_left = subLR;
    if (subLR != nullptr) {
        subLR->_parent = parent;
    }
    // 2. 处理subL与parent父结点的关系
    Node* parentParent = parent->_parent;
    if (parentParent == nullptr) {
        // parent是根结点,旋转后subL成为新根
        _root = subL;
    } else if (parent == parentParent->_left) {
        // parent是左孩子
        parentParent->_left = subL;
    } else {
        // parent是右孩子
        parentParent->_right = subL;
    }
    subL->_parent = parentParent;
    // 3. 处理subL与parent的关系
    subL->_right = parent;
    parent->_parent = subL;
}

2.3.2 左单旋(RotateL)

        当右子树过高时,通过左单旋将右子树的根结点提升为新的父结点,原父结点变为新根的左子结点。

template
void RBTree::RotateL(Node* parent) {
    Node* subR = parent->_right;      // 右子树的根结点
    Node* subRL = subR->_left;        // 右子树的左孩子
    // 1. 处理subRL与parent的关系
    parent->_right = subRL;
    if (subRL != nullptr) {
        subRL->_parent = parent;
    }
    // 2. 处理subR与parent父结点的关系
    Node* parentParent = parent->_parent;
    if (parentParent == nullptr) {
        // parent是根结点,旋转后subR成为新根
        _root = subR;
    } else if (parent == parentParent->_left) {
        // parent是左孩子
        parentParent->_left = subR;
    } else {
        // parent是右孩子
        parentParent->_right = subR;
    }
    subR->_parent = parentParent;
    // 3. 处理subR与parent的关系
    subR->_left = parent;
    parent->_parent = subR;
}

        旋转操作的要点

  • 旋转时需维护父指针的指向,避免出现悬空指针;
  • 旋转仅调整结构,不改变结点颜色,也不破坏二叉搜索树的特性。

三、红黑树的插入实现

        红黑树的插入流程分为两步:

        ① 按二叉搜索树规则插入结点;

        ② 调整结点颜色和结构,恢复红黑树规则。

3.1 插入的基础流程

  1. 若树为空,直接创建根结点并设为黑色(满足规则 2);
  2. 若树非空,按二叉搜索树规则找到插入位置,创建新结点(默认红色)并插入
  3. 插入后检查红黑树规则:
  • 若新结点的父结点为黑色,规则未被破坏,插入结束;
  • 若新结点的父结点为红色,破坏规则 4(连续红色结点),需进行平衡调整。

3.2 平衡调整的核心场景

        插入调整的关键在于分析新结点(cur)、父结点(parent)、祖父结点(grandfather)、叔叔结点(uncle)的颜色和位置关系。由于父结点为红色,祖父结点必为黑色(规则 4 不允许连续红色),因此核心变量是叔叔结点的颜色。

        为了简化分析,我们定义以下标识:

  • cur:新增结点(红色);
  • parent:cur 的父结点(红色);
  • grandfather:parent 的父结点(黑色);
  • uncle:parent 的兄弟结点(祖父结点的另一个子结点)。

        根据 parent 是 grandfather 的左孩子或右孩子,以及 uncle 的颜色,下面我们将平衡调整问题分为三大场景,每个场景对应不同的调整策略。

场景 1:叔叔结点存在且为红色(变色调整)

        条件:parent 为红,grandfather 为黑,uncle 存在且为红。

        问题:cur 与 parent 连续红色(破坏规则 4)。

        调整策略

  1. parent uncle 设为黑色;
  2. grandfather 设为红色;
  3. grandfather 作为新的 cur,向上回溯调整(可能 grandfather 的父结点也是红色)。

        调整原理

  • parent 和 uncle 变黑,保证了原路径的黑高不变(满足规则 5);
  • grandfather 变红,可能导致其与父结点连续红色,因此需要继续向上调整;
  • 若 grandfather 是根结点,调整后需将其设为黑色(满足规则 2)。

        代码片段

// 假设parent是grandfather的左孩子
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) {
    // 场景1:叔叔存在且为红,变色调整
    parent->_col = BLACK;
    uncle->_col = BLACK;
    grandfather->_col = RED;
    // 向上回溯
    cur = grandfather;
    parent = cur->_parent;
}

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

        条件:parent 为红,grandfather 为黑,uncle 不存在或为黑,且 cur 与 parent 同方向(cur 是 parent 的左孩子,parent 是 grandfather 的左孩子;或 cur 是 parent 的右孩子,parent 是 grandfather 的右孩子)。

        问题:cur 与 parent 是连续红色的,并且无法通过单纯变色解决(uncle 为黑,变色会破坏规则 5)。

        调整策略

  1. grandfather 为旋转中心,进行单旋(parent 是左孩子则右旋,parent 是右孩子则左旋);
  2. parent 设为黑色(新的子树根结点);
  3. grandfather 设为红色

        调整原理

  • 旋转后 parent 成为子树根结点,设为黑色保证黑高不变;
  • grandfather 成为 parent 的子结点,设为红色避免连续红色;
  • 调整后无需向上回溯(parent 的父结点若存在,必为黑色,否则之前会被处理)。

代码片段(parent 是 grandfather 的左孩子,cur 是 parent 的左孩子)

else if (cur == parent->_left) {
    // 场景2:单旋(右旋)+ 变色
    RotateR(grandfather);
    parent->_col = BLACK;
    grandfather->_col = RED;
    break;  // 调整完成,无需向上回溯
}

代码片段(parent 是 grandfather 的右孩子,cur 是 parent 的右孩子)

else if (cur == parent->_right) {
    // 场景2:单旋(左旋)+ 变色
    RotateL(grandfather);
    parent->_col = BLACK;
    grandfather->_col = RED;
    break;
}

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

        条件:parent 为红,grandfather 为黑,uncle 不存在或为黑,且 cur 与 parent 反方向(cur 是 parent 的右孩子,parent 是 grandfather 的左孩子;或 cur 是 parent 的左孩子,parent 是 grandfather 的右孩子)。

        问题:cur 与 parent 连续红色,且单旋无法直接调整(cur 在 parent 的另一侧)。

        调整策略

  1. parent 为旋转中心,进行一次单旋(cur 是 parent 的右孩子则左旋,cur 是 parent 的左孩子则右旋),将 cur 调整到 parent 的位置;
  2. grandfather 为旋转中心,进行二次单旋(与场景 2 一致);
  3. cur 设为黑色(新的子树根结点);
  4. grandfather 设为红色。

        调整原理

  • 第一次旋转将 cur 与 parent 的位置互换,转化为场景 2 的情况;
  • 第二次旋转调整结构,cur 设为黑色保证黑高不变;
  • 调整后无需向上回溯。

代码片段(parent 是 grandfather 的左孩子,cur 是 parent 的右孩子)

else {
    // 场景3:双旋(先左旋parent,再右旋grandfather)+ 变色
    RotateL(parent);
    RotateR(grandfather);
    cur->_col = BLACK;
    grandfather->_col = RED;
    break;
}

代码片段(parent 是 grandfather 的右孩子,cur 是 parent 的左孩子)

else {
    // 场景3:双旋(先右旋parent,再左旋grandfather)+ 变色
    RotateR(parent);
    RotateL(grandfather);
    cur->_col = BLACK;
    grandfather->_col = RED;
    break;
}

3.3 完整插入代码实现

template
bool RBTree::Insert(const pair& kv) {
    // 步骤1:空树处理
    if (_root == nullptr) {
        _root = new Node(kv);
        _root->_col = BLACK;  // 根结点必须为黑色
        return true;
    }
    // 步骤2:按二叉搜索树规则查找插入位置
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur != nullptr) {
        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:平衡调整(父结点为红色时才需要调整)
    while (parent != nullptr && parent->_col == RED) {
        Node* grandfather = parent->_parent;  // 祖父结点必为黑色
        if (parent == grandfather->_left) {
            // 情况A:parent是grandfather的左孩子
            Node* uncle = grandfather->_right;
            if (uncle != nullptr && uncle->_col == RED) {
                // 场景1:叔叔存在且为红,变色调整
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandfather->_col = RED;
                // 向上回溯
                cur = grandfather;
                parent = cur->_parent;
            } else {
                // 场景2和3:叔叔不存在或为黑,旋转+变色
                if (cur == parent->_left) {
                    // 场景2:cur是parent的左孩子,右单旋
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } else {
                    // 场景3:cur是parent的右孩子,双旋
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;  // 调整完成,无需向上回溯
            }
        } else {
            // 情况B:parent是grandfather的右孩子(与左孩子对称)
            Node* uncle = grandfather->_left;
            if (uncle != nullptr && uncle->_col == RED) {
                // 场景1:叔叔存在且为红,变色调整
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandfather->_col = RED;
                // 向上回溯
                cur = grandfather;
                parent = cur->_parent;
            } else {
                // 场景2和3:叔叔不存在或为黑,旋转+变色
                if (cur == parent->_right) {
                    // 场景2:cur是parent的右孩子,左单旋
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } else {
                    // 场景3:cur是parent的左孩子,双旋
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;  // 调整完成,无需向上回溯
            }
        }
    }
    // 确保根结点始终为黑色(可能在场景1中被设为红色)
    _root->_col = BLACK;
    return true;
}

3.4 插入代码关键要点

  1. 根结点最终必须设为黑色:场景 1 中可能将根结点设为红色,因此插入结束后需强制将根结点设为黑色;
  2. 回溯终止条件:parent 为空(cur 成为根结点)或 parent 为黑色(规则 4 不再被破坏);
  3. 对称处理:parent 是 grandfather 的左孩子和右孩子的逻辑对称,仅需调整旋转方向和叔叔结点的位置;
  4. 新结点默认红色:这是红黑树插入效率高的关键设计,避免直接破坏规则 5。

四、红黑树的查找与验证实现

4.1 查找操作实现

        红黑树的查找逻辑与二叉搜索树完全一致,利用其 “左小右大” 的特性遍历树即可,时间复杂度为 O (logN)

template
typename RBTree::Node* RBTree::Find(const K& key) {
    Node* cur = _root;
    while (cur != nullptr) {
        if (cur->_kv.first < key) {
            // 关键字大于当前结点,向右走
            cur = cur->_right;
        } else if (cur->_kv.first > key) {
            // 关键字小于当前结点,向左走
            cur = cur->_left;
        } else {
            // 找到目标结点
            return cur;
        }
    }
    // 未找到
    return nullptr;
}

4.2 红黑树的验证实现

        红黑树的验证需检查所有规则是否满足,核心是规则 4(无连续红色结点)和规则 5(黑高一致)。验证流程如下:

  1. 根结点必须为黑色;
  2. 前序遍历检查无连续红色结点;
  3. 计算所有路径的黑高,确保一致。

4.2.1 验证辅助函数(Check)

        递归遍历树,检查连续红色结点和黑高一致性:

template
bool RBTree::Check(Node* root, int blackNum, const int refNum) {
    // 递归到空结点,检查当前路径黑高是否与参考黑高一致
    if (root == nullptr) {
        if (blackNum != refNum) {
            cout << "错误:存在黑高不一致的路径,当前黑高=" << blackNum << ",参考黑高=" << refNum << endl;
            return false;
        }
        return true;
    }
    // 检查连续红色结点(当前结点为红,父结点也为红)
    if (root->_col == RED && root->_parent != nullptr && root->_parent->_col == RED) {
        cout << "错误:存在连续红色结点,关键字=" << root->_kv.first << endl;
        return false;
    }
    // 遇到黑色结点,黑高计数+1
    if (root->_col == BLACK) {
        blackNum++;
    }
    // 递归检查左右子树
    return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}

4.2.2 验证主函数(IsBalance)

        计算参考黑高(根结点到最左路径的黑高),调用辅助函数进行验证:

template
bool RBTree::IsBalance() {
    // 空树视为平衡
    if (_root == nullptr) {
        return true;
    }
    // 检查根结点是否为黑色
    if (_root->_col != BLACK) {
        cout << "错误:根结点不是黑色" << endl;
        return false;
    }
    // 计算参考黑高(根结点到最左路径的黑高)
    int refNum = 0;
    Node* cur = _root;
    while (cur != nullptr) {
        if (cur->_col == BLACK) {
            refNum++;
        }
        cur = cur->_left;
    }
    // 递归检查所有路径
    int blackNum = 0;
    return Check(_root, blackNum, refNum);
}

        验证函数说明

  • 参考黑高选择根结点到最左路径的黑高,可任意选择一条路径作为参考;
  • 递归过程中,blackNum 记录当前路径的黑高,refNum 为参考黑高;
  • 若存在连续红色结点或黑高不一致,则直接返回 false 并输出错误信息。

五、红黑树的应用与扩展

5.1 红黑树的工业应用

  1. C++ STL 容器:set、map、multiset、multimap 的底层实现;
  2. Linux 内核:进程调度中的 CFS 调度器(使用红黑树管理进程优先级);
  3. 数据库:MySQL 的 B + 树索引底层使用红黑树维护索引结构;
  4. 缓存系统:用于高效管理缓存数据的插入、删除和查找。

5.2 红黑树的扩展(删除操作)

        红黑树的删除操作比插入更为复杂,核心难点在于删除黑色结点后会破坏规则 5(黑高一致),需要通过 “双重黑色” 结点的概念进行调整。在这我就不详细讲解了,大家感兴趣的话可以参考《算法导论》第 13 章或《STL 源码剖析》的相关内容进行学习。

        简单说一下删除操作的核心思路:

  1. 按二叉搜索树规则删除结点,找到替代结点(中序后继或前驱);
  2. 若删除的是红色结点,直接删除,规则未被破坏;
  3. 若删除的是黑色结点,需调整以恢复黑高一致,可能涉及变色、旋转或向上回溯。

总结

        红黑树的核心难点在于插入后的平衡调整,需重点掌握三大场景的处理逻辑:叔叔结点为红时的变色调整、叔叔结点为黑或不存在时的单旋 + 变色和双旋 + 变色。通过反复练习和调试代码,相信大家一定可以逐步理解红黑树的平衡原理。感谢大家的支持!