当前位置: 首页 > news >正文

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

目录

前言

一、红黑树的核心概念

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<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) {}               // 新增结点默认红色(关键设计)
};

        关键说明

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

2.2 红黑树类的框架

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

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 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<class K, class V>
void RBTree<K, V>::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<class K, class V>
void RBTree<K, V>::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<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 != 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<class K, class V>
typename RBTree<K, V>::Node* RBTree<K, V>::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<class K, class V>
bool RBTree<K, V>::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;}// 遇到黑色结点,黑高计数+1if (root->_col == BLACK) {blackNum++;}// 递归检查左右子树return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}

4.2.2 验证主函数(IsBalance)

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

template<class K, class V>
bool RBTree<K, V>::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. 若删除的是黑色结点,需调整以恢复黑高一致,可能涉及变色、旋转或向上回溯。

总结

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

http://www.dtcms.com/a/581794.html

相关文章:

  • HBase Shell里表操作实战
  • ESP32 FreeRTOS IPC机制全解析
  • 建设银行信用卡卡网站温州微网站制作公司哪家好
  • 虚幻引擎5 GAS开发俯视角RPG游戏 P07-07 激活能力
  • [特殊字符] 常用 Maven 命令
  • 简单的智能数据分析程序
  • 网页制作元素有哪些前端角度实现网站首页加载慢优化
  • C++中的智能指针std::shared_ptr是线程安全的吗?以及它的详细实现原理
  • 网站服务器安装教程视频教程电子商务网站规划
  • 【vsftpd报错】227 Entering Passive Mode,553 Could not create file.
  • 有多少网站可以推广业务那个公司做app
  • 正规的大连网站建设a963中华室内设计官网
  • 中承信安信创软件检测:CMA资质+国家标准双重保障的测试报告
  • #智能CI/CD流水线与AIOps 论坛@AiDD深圳站
  • 医疗AI模型与控制器自动化CI/CD流水线
  • NumPy -数组运算与操作
  • 中美最近军事新闻邯郸网站优化公司
  • windows本机vscode通过ssh免密登录远程linux服务器 git push/pull 免密
  • go语言网站开发教程门户网站是如何做引流的
  • SG-ECAT_S-TCP(EtherCAT 转 ModbusTCP 网关)
  • 分享一些在C++中使用异常处理的最佳实践
  • 物流网站怎么开网络最好的运营商
  • 学习随笔-async和await
  • 祁阳做网站河南工程建设验收公示网
  • PCIe协议分析仪-VIAVI设置抓取ASPM协商过程
  • ThreadLocal 相关知识点
  • OSG新版GLSL语法全解析
  • 智守边界:入侵报警系统的主动防御时代
  • 为什么网站建设起来搜素不到电子商务网站建设考题
  • 济南网站建设是什么合肥seo网络营销推广