【C++】红黑树详解(2w字详解)

【C++】红黑树详解(2w字详解)

手搓AVL树

手搓红黑树

github地址

有梦想的电信狗

0. 前言

前文链接:手搓 AVL 树

在上一篇文章中,我们从零实现了 AVL 树,深入理解了二叉搜索树的平衡思想。然而,AVL 树虽然严格平衡,但插入和删除操作频繁时,频繁的旋转会带来较大的性能损耗。

为了在“平衡性”和“性能”之间取得更好的折中,红黑树(Red-Black Tree) 应运而生。它通过为每个结点引入“颜色属性”,并对节点间的颜色分布施加约束,使得树在近似平衡的状态下仍能保持高效的查找、插入与删除性能。

红黑树并不追求绝对平衡,而是通过「颜色规则 + 局部旋转」实现“动态平衡”。

这种思想也正是 C++ STL 容器 map / set 的底层核心机制

本文将从最基本的概念出发,逐步剖析红黑树的性质、插入规则、旋转逻辑,并最终完成一个完整可运行的红黑树实现。
我们还将通过与 AVL 树的性能对比,深入理解红黑树在实际工程场景中的优势与应用价值。


📘 本文目标:理解红黑树的设计思想掌握插入时的变色与旋转规则实现一个可验证平衡性的红黑树对比红黑树与 AVL 树的性能差异

1. 什么是红黑树

概念与定义

我们之前学习过了二叉搜索树和 AVL 树红黑树和 AVL 树一样,也是一棵自平衡的二叉搜索树AVL 树通过控制左右子树的高度差不超过 1 来保持平衡,而红黑树则是为每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。 通过对任何一条从根到叶子的路径上各个结点的颜色进行约束

红黑树:一种 自平衡二叉搜索树,由德国计算机科学家 Rudolf Bayer 在 1972 年发明,当时被称为对称二叉B树 ,后来被 Leo J. GuibasRobert Sedgewick 改进并命名为红黑树

  • 它在很多编程语言的库和实际应用场景中都有广泛使用
    • C++ 的 STL 库中的 mapset 底层实现
    • JavaTreeMap 等库的实现
  • 它通过额外的 颜色标记旋转操作 来维持树的平衡,确保最坏情况下的基本操作(插入、删除、查找)时间复杂度为== O ( l o g N ) O(logN) O(logN)==
  • 它在每个节点上增加了一个存储位来表示节点的颜色,这个颜色可以是红色黑色
    • 通过对从根节点到任意叶子节点路径上的节点颜色进行约束来保持树的左右两边的相对平衡,红黑树确保没有一条路径会比其他路径长出 2 倍,即 2 * 最短路径 >= 最长路径,因而它是一种近似平衡的二叉搜索树

红黑树示例

在这里插入图片描述

2. 红黑树的性质

红黑树的性质

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点必须是黑色的
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

红黑树的性质解读

性质解读

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点必须是黑色的
    • 由性质3我们可以推断出:红黑树的任何路径没有连续的红色结点
  4. 对于每个结点,从该结点到其所有后代叶结点(NIL)的简单路径上,均 包含相同数目的黑色结点
    • 由性质4我们可以推断出:从根节点开始,每条路径上,都有相同数量的黑色结点
  5. 每个叶子结点(NIL结点)都是黑色的(此处的叶子结点指的是空结点)
    • 说明:《算法导论》等书籍上补充了一条每个叶子结点 (NIL) 都是黑色的规则。他这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把 NIL 叫做外部结点。NIL 是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了 NIL 结点,所以我们知道一下这个概念即可。
    • 黑色的NIL结点即为红黑树的空结点
在这里插入图片描述

性质总结

左根右:由于它是二叉搜索树,所以左子树的值 < 根的值 < 右子树的值;根叶黑:根节点和叶子节点 (NIL) 都是黑色的;不红红:一条路径上不会出现两个连续的红色节点;黑路同:任意两条路径上的黑色节点个数相同。

因此,红黑树的性质可以简记为:左根右,根叶黑,不红红,黑路同

树的路径再认识

在这里插入图片描述

树路径的计算

  • 计算路径:从根节点到空结点,为一条路径,因此以上树有11条路径

如果树路径的计算不包含空节点,那么以下这棵树会被解读成红黑树,但其实这棵树不是红黑树(违反性质4)

在这里插入图片描述

3. 红黑树如何确保最长路径不超过最短路径的2倍?

红黑树路径规则推导

  • 最短路径(全黑路径)
    • 由红黑树性质 4(从根到 NIL结点的每条路径,黑色结点数量相同 )可知,极端场景下,最短路径就是全由黑色结点组成的路径。 我们把这条最短路径的长度记为 bhblack height,黑色高度 )
  • 最长路径(黑红间隔路径)
    • 结合规则 2(根结点是黑色 )和规则 3(红色结点的子结点必为黑色,路径中不会出现连续红色结点 )。极端场景下,最长路径会呈现一黑一红交替的结构,因此最长路径长度为 2*bh单条路径黑色结点数量最多为 bh,红色结点数量最多也为 bh
  • 路径长度范围
    • 实际红黑树中,“全黑最短路径” 和 “黑红交替最长路径” 不一定同时存在。但通过 4 条规则可推导:任意从根到 NIL 结点的路径长度 h,都满足 bh ≤ h ≤ 2*bh,这保证了红黑树的近似平衡特性

4. 红黑树的实现

整体架构设计

结点颜色的枚举类

  • 我们定义一个枚举类,枚举值表示结点的颜色
enumColour{ Red, Black };

红黑树的结点定义

  • 红黑树结点为模版实现
template<classK,classV>structRBTreeNode{// 三叉链 RBTreeNode<K, V>* _left; RBTreeNode<K, V>* _right; RBTreeNode<K, V>* _parent;// 红黑树需要旋转,因此设计为三叉链,保存父节点的地址 pair<K, V> _kv;// 键值对 Colour _col;// 表示结点的颜色// 新结点默认是红色的RBTreeNode(const pair<K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_col(Red){}};
  • 搜索树常用于存储键值,方便查找关键字,这里我们使用std::pair<K, V>来存储我们的键值对数据
  • 结点中的成员变量:采用三叉链的方式实现
    • RBTreeNode<K, V>* _left:指向左孩子的指针
    • RBTreeNode<K, V>* _right:指向右孩子的指针
    • RBTreeNode<K, V>* _parent:指向父节点的指针
  • 默认构造函数RBTreeNode(const pair<K, V>& kv)
    • 将三个指针初始化为nullptr初始化结点颜色为Red
    • 使用kv初始化类内的_kv成员
  • 结点采用struct设计,默认权限为public,方便下文的RBTree类访问成员
思考:在节点的定义中,为什么要将节点的默认颜色给成红色的

红黑树设计

  • 红黑树为模版实现
template<classK,classV>classRBTree{typedef RBTreeNode<K, V> Node; RBTreeNode<K, V>* _root =nullptr;public:// ... 对外共有接口private:// ... 内部私有成员函数};
  • typedef RBTreeNode<K, V> Node;:结点类型重定义简化书写
  • RBTree<K, V>* _root = nullptr初始时根节点为空

红黑树的插入实现

1. 空树的插入

当我们对一棵空的红黑树进行插入时,直接插入并把其颜色设置为黑色即可。

(下面所讨论的插入操作都是建立在树非空的情况之上)


2. 新插入节点的父亲为黑色

新结点的颜色
首先我们需要明确一点,在红黑树非空的情况下,我们插入一个节点的颜色必定是红色。
在这里插入图片描述

如果我们把插入节点的颜色设置为黑色的话,那么此时这棵红黑树就必然会违反黑路同的性质,插入黑色结点会对其他路径造成影响(黑路同),之后想要把每条路上的黑色节点数量调整为一样多的话就会非常麻烦。所以从这个角度来看,树非空的情况下我们插入的节点都设置为红色。

  • 因此,插入一个红色节点,如果它的父亲是黑色,那么将不会违反红黑树的任何一条性质,插入结束

3. 新插入节点的父亲为红色

(1)叔叔存在且为红色:变色 + 继续向上处理

我们插入的节点为红色,

  • 此时如果它的父亲也为红色,那么就会违反不红红的性质。那么此时让父亲变为黑色
  • 但这也意味着父亲这条路径上多了一个黑色节点,违反了黑路同,我们需要调节黑色节点的数量。
  • 此时可以让父亲的父亲 (grandfather) 变为红色 (由于变色前父亲为红,所以爷爷变色之前必然为黑),再让叔叔节点变为黑色,这样一来,我们就可以同时满足不红红和黑路同两个性质了。

但是,此时由于爷爷变红,可能又会导致出现两个红色节点相邻的情况,因为爷爷的父亲可能为红色。此时我们只需要把爷爷当作新插入的节点,重复上面的操作即可。下面是图片演示

在这里插入图片描述
在这里插入图片描述
  • 一直重复这样的操作,直到不违反红黑树的性质或者到根为止。注意grandfather到根的时候需要把根变为黑色
  • 下面是继续向上调整的红黑树抽象图
在这里插入图片描述

(2)叔叔不存在或叔叔为黑色:旋转 + 变色
①LL型:右单旋 + 变色
  • uncle不存在时
在这里插入图片描述
  • uncle为黑色时
在这里插入图片描述
②RR型:左单旋 + 变色
  • uncle不存在时
在这里插入图片描述
  • uncle为黑色时
在这里插入图片描述
③LR型:左右双旋 + 变色
  • uncle为不存在时
在这里插入图片描述
  • uncle为黑色时
在这里插入图片描述
在这里插入图片描述
①RL型:右左双旋 + 变色
  • uncle为不存在时
在这里插入图片描述
  • uncle为黑色时

如果此时插入节点的父亲节点在爷爷节点的右边 (right),插入节点在父亲节点的左边 (left),那么我们把这种情况成为 RL 型。如果是 RL 型,那么需要将以爷爷节点为根的子树进行右左双旋操作,旋转之后将 cur 和爷爷节点进行变色,插入结束。

这种情况与 LR 型完全对称,这里不再画图演示


4. 旋转操作

这里的旋转操作和 AVL 树的旋转操作一致,只需把 AVL 树中的更新平衡因子的步骤去掉
左单旋
// 左单旋 2 1 newNode 练成线,单纯的右边高voidRotateL(Node* parent){ _rotateCount++;// 测试旋转次数if(parent ==nullptr|| parent->_right ==nullptr)return; Node* curNode = parent->_right; Node* curLeft = curNode->_left;// curLeft 有可能为空// 先处理 curNode 的 left 结点,curLeft 有可能是空 parent->_right = curLeft;if(curLeft) curLeft->_parent = parent;// 再处理 curNode 结点// parent 有可能是根节点,也有可能是子树的根节点if(parent == _root){// 先立新根 _root = curNode; curNode->_parent =nullptr;// 再挂旧根 parent->_parent = curNode; curNode->_left = parent;}else{ Node* ppNode = parent->_parent;// 这里不知道 parent 是 ppNode 的 左孩子 还是 右孩子 if(parent == ppNode->_left) ppNode->_left = curNode;else ppNode->_right = curNode; curNode->_parent = ppNode;// 挂 parent parent->_parent = curNode; curNode->_left = parent;}}
右单旋
// 右单旋 -2 -1 newNode 连成线,单纯的左边高voidRotateR(Node* parent){ _rotateCount++;// 测试旋转次数// parent 为空 或 curNode 为空的情况if(parent ==nullptr|| parent->_left ==nullptr)return; Node* curNode = parent->_left; Node* curRight = curNode->_right;// 把 curNode 的 right 给给 parent 的 left parent->_left = curRight;if(curRight) curRight->_parent = parent;if(parent == _root){// 先立新根 _root = curNode; curNode->_parent =nullptr;// 再挂旧根 curNode->_right = parent; parent->_parent = curNode;}else{ Node* ppNode = parent->_parent;// 找 parent 是 ppNode 的左还是右if(parent == ppNode->_left) ppNode->_left = curNode;else ppNode->_right = curNode; curNode->_parent = ppNode;// 挂 parent curNode->_right = parent; parent->_parent = curNode;}}

5. 插入的完整代码

boolinsert(const pair<K, V>& kv){// 先走二叉搜索树的插入逻辑if(_root ==nullptr){ _root =newNode(kv); _root->_col = Black;// 性质 根节点是黑色的returntrue;}// _root 不为空时,二叉搜索树的逻辑 Node* parent =nullptr; Node* curNode = _root;// 先找空,找到一个可以插入的位置while(curNode){if(kv.first < curNode->_kv.first){ parent = curNode; curNode = curNode->_left;}elseif(kv.first > curNode->_kv.first){ parent = curNode; curNode = curNode->_right;}// 搜索树中不允许有重复的值 对于已有值,不插入elsereturnfalse;}// while 循环结束后,代表找到了可以插入的位置// 找到位置了,但父节点不知道 新结点比自己大还是比自己小 curNode =newNode(kv);if(curNode->_kv.first < parent->_kv.first) parent->_left = curNode;else parent->_right = curNode; curNode->_parent = parent;// 以上是二叉搜索树的插入逻辑,这样插入可能导致树不平衡,从而导致查找效率退化为 O(n)// 以下是红黑树的性质控制,是对二叉搜索树 进行的 控制平衡 操作// 控制近似平衡 ... // while 循环控制 颜色继续往上更新// 新插入的结点为红色 因此 parent 存在且 parent 为红色时,才需要更新颜色// while(parent && parent->_col == Red){ Node* grandFather = parent->_parent;// parent 在 grandFather 左的场景if(parent == grandFather->_left){ Node* uncle = grandFather->_right;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if(uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色 parent->_col = uncle->_col = Black; grandFather->_col = Red;// 继续向上处理 curNode = grandFather; parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 左左 -> 右单旋if(curNode == parent->_left){// g// p// cRotateR(grandFather); parent->_col = Black; grandFather->_col = Red;}// 左右 -> 左右双旋else{// g// p// cRotateL(parent);RotateR(grandFather); curNode->_col = Black; grandFather->_col = Red;}break;}}// parent 在 grandFather 右的场景else{ Node* uncle = grandFather->_left;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if(uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色 parent->_col = uncle->_col = Black; grandFather->_col = Red;// 继续向上处理 curNode = grandFather; parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 右右 -> 左单旋if(curNode == parent->_right){// g// p// cRotateL(grandFather); parent->_col = Black; grandFather->_col = Red;}// 右左 -> 右左双旋else{// g// p// cRotateR(parent);RotateL(grandFather); curNode->_col = Black; grandFather->_col = Red;}break;}}}// 继续向上处理 parent = curNode->_parent 如果 parent == nullptr 会结束循环// parent == nullptr 结束循环时,curNode为_root结点,需要对 _root 节点变色 _root->_col = Black;// 粗暴的处理,直接将根节点设为黑色,根节点为黑色总是正确的returntrue;}

5. 验证红黑树

求树的高度

求树的高度思路如下

  • 先分别求树的左右子树高度
  • 最终返回左右子树中 高度更大的高度 + 1
public:intHeight(){return_Height(_root);}int_Height(Node* root = _root){if(root ==nullptr)return0;int leftHeight =_Height(root->_left);int rightHeight =_Height(root->_right);return leftHeight > rightHeight ? leftHeight +1: rightHeight +1;}

判断树是否是红黑树

判断是否平衡

  • 通过判断黑色节点的数量来判断黑色节点是否满足红黑树性质
boolisBalance(){return_isBalance(_root);}// 子函数bool_isBalance(Node* root = _root){if(root ==nullptr)returntrue;// 根节点颜色不为黑,falseif(root->_col != Black)returnfalse;// 基准值int benchmark =0; Node* cur = _root;while(cur){if(cur->_col == Black)++benchmark; cur = cur->_left;}returnCheckColour(root,0, benchmark);}

_isBalance实现以下功能:

  • 检查根节点的颜色是否为黑色
  • 计算树中最左路径中,黑色结点的数量作为基准值,传给 CheckColour 函数,与其他路径的黑色节点数量进行比对

检查红黑树的颜色

  • 递归检查红黑树的颜色
boolCheckColour(Node* root,int blacknum,int benchmark){if(root ==nullptr){if(blacknum != benchmark)returnfalse;returntrue;}// 检查黑色节点的数量if(root->_col == Black)++blacknum;// 检查有没有连续的红色结点if(root->_col == Red && root->_parent && root->_parent->_col == Red){ cout << root->_kv.first <<"出现连续红色节点"<< endl;returnfalse;}returnCheckColour(root->_left, blacknum, benchmark)&&CheckColour(root->_right, blacknum, benchmark);}

CheckColour函数内部完成以下检查:

  • 递归检查,检查树中是否有连续的红色结点if (root->_col == Red && root->_parent && root->_parent->_col == Red)
    • 若当前节点和它的父节点都是红色,则出现了连续的红色节点,违反红黑树性质④,输出提示并返回 false
  • 递归检查,检查其他路径中黑色结点的数量与基准值是否相同
    • 如果当前节点是黑色,就增加路径上的黑节点计数。(每次进入新节点,统计路径上黑节点的数量)
    • 当遍历到 nullptr 时,说明到达叶子。此时:
      • blacknum 是从根到当前叶子的黑节点数量;
      • benchmark 是从根到某一条路径上统计得到的标准黑节点数(通常是第一次到达叶子时确定的值)。
      • 如果当前路径上的黑节点数与标准值不同,则不满足红黑树性质⑤,返回 false
  • 对左右子树分别检查,只有两边都满足条件,整棵树才合法。注意:blacknum值传递在每条递归路径中互不影响

blacknum使用“传值”:

形参传值会拷贝一份当前的 blacknum,在递归调用中独立变化。

也就是说:

  • 每条递归路径拥有自己独立的黑节点计数,不会因为返回后互相干扰;
  • 非常适合这种“每条路径独立计算”的场景。

6. 红黑树与AVL树性能测试对比

  • 红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2​N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

测试代码

#include"Red_Black_Tree.h"#include"AVL_Tree.h"#include<vector>intmain(){constint N =10000000; vector<int> v; v.reserve(N);srand(time(0));for(size_t i =0; i < N; i++){ v.push_back(rand()+ i*i +2);// 插入随机数据//v.push_back(i); // 插入有序数据} RBTree<int,int> rbt;for(auto e : v){ rbt.insert(make_pair(e, e));//cout << "Insert:" << e << "->" << t.isBalance() << endl;} cout <<"红黑树是否平衡: "<< rbt.isBalance()<< endl; cout <<"红黑树的高度: "<<rbt.Height()<< endl; cout <<"红黑树的旋转次数: "<< rbt._rotateCount << endl; cout << endl << endl; AVLTree<int,int> avlt;for(auto e : v){ avlt.insert(make_pair(e, e));//cout << "Insert:" << e << "->" << t.IsBalance() << endl;} cout <<"AVL树是否平衡: "<< avlt.isBalance()<< endl; cout <<"AVL树的高度: "<< avlt.height()<< endl; cout <<"AVL树的旋转次数: "<< avlt._rotateCount << endl;return0;}

插入有序数据的结果

在这里插入图片描述

插入随机无序数据的结果

在这里插入图片描述

7. 完整代码

#pragmaonce#include<iostream>usingnamespace std;enumColour{ Red, Black };template<classK,classV>structRBTreeNode{ RBTreeNode<K, V>* _left; RBTreeNode<K, V>* _right; RBTreeNode<K, V>* _parent; pair<K, V> _kv; Colour _col;// 新结点默认是红色的RBTreeNode(const pair<K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_col(Red){}};template<classK,classV>classRBTree{public:int _rotateCount =0;private:typedef RBTreeNode<K, V> Node; RBTreeNode<K, V>* _root =nullptr;public:boolinsert(const pair<K, V>& kv){// 先走二叉搜索树的插入逻辑if(_root ==nullptr){ _root =newNode(kv); _root->_col = Black;// 性质 根节点是黑色的returntrue;}// _root 不为空时,二叉搜索树的逻辑 Node* parent =nullptr; Node* curNode = _root;// 先找空,找到一个可以插入的位置while(curNode){if(kv.first < curNode->_kv.first){ parent = curNode; curNode = curNode->_left;}elseif(kv.first > curNode->_kv.first){ parent = curNode; curNode = curNode->_right;}// 搜索树中不允许有重复的值 对于已有值,不插入elsereturnfalse;}// while 循环结束后,代表找到了可以插入的位置// 找到位置了,但父节点不知道 新结点比自己大还是比自己小 curNode =newNode(kv);if(curNode->_kv.first < parent->_kv.first) parent->_left = curNode;else parent->_right = curNode; curNode->_parent = parent;// 以上是二叉搜索树的插入逻辑,这样插入可能导致树不平衡,从而导致查找效率退化为 O(n)// 以下是红黑树的性质控制,是对二叉搜索树 进行的 控制平衡 操作// 控制近似平衡 ... // while 循环控制 颜色继续往上更新// 新插入的结点为红色 因此 parent 存在且 parent 为红色时,才需要更新颜色// while(parent && parent->_col == Red){ Node* grandFather = parent->_parent;// parent 在 grandFather 左的场景if(parent == grandFather->_left){ Node* uncle = grandFather->_right;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if(uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色 parent->_col = uncle->_col = Black; grandFather->_col = Red;// 继续向上处理 curNode = grandFather; parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 左左 -> 右单旋if(curNode == parent->_left){// g// p// cRotateR(grandFather); parent->_col = Black; grandFather->_col = Red;}// 左右 -> 左右双旋else{// g// p// cRotateL(parent);RotateR(grandFather); curNode->_col = Black; grandFather->_col = Red;}break;}}// parent 在 grandFather 右的场景else{ Node* uncle = grandFather->_left;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if(uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色 parent->_col = uncle->_col = Black; grandFather->_col = Red;// 继续向上处理 curNode = grandFather; parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 右右 -> 左单旋if(curNode == parent->_right){// g// p// cRotateL(grandFather); parent->_col = Black; grandFather->_col = Red;}// 右左 -> 右左双旋else{// g// p// cRotateR(parent);RotateL(grandFather); curNode->_col = Black; grandFather->_col = Red;}break;}}}// 继续向上处理 parent = curNode->_parent 如果 parent == nullptr 会结束循环// parent == nullptr 结束循环时,curNode为_root结点,需要对 _root 节点变色 _root->_col = Black;// 粗暴的处理,直接将根节点设为黑色,根节点为黑色总是正确的returntrue;}boolCheckColour(Node* root,int blacknum,int benchmark){if(root ==nullptr){if(blacknum != benchmark)returnfalse;returntrue;}// 检查黑色节点的数量if(root->_col == Black)++blacknum;// 检查有没有连续的红色结点if(root->_col == Red && root->_parent && root->_parent->_col == Red){ cout << root->_kv.first <<"出现连续红色节点"<< endl;returnfalse;}returnCheckColour(root->_left, blacknum, benchmark)&&CheckColour(root->_right, blacknum, benchmark);}boolisBalance(){return_isBalance(_root);}bool_isBalance(Node* root = _root){if(root ==nullptr)returntrue;if(root->_col != Black)returnfalse;// 基准值int benchmark =0; Node* cur = _root;while(cur){if(cur->_col == Black)++benchmark; cur = cur->_left;}returnCheckColour(root,0, benchmark);}intHeight(){return_Height(_root);}int_Height(Node* root = _root){if(root ==nullptr)return0;int leftHeight =_Height(root->_left);int rightHeight =_Height(root->_right);return leftHeight > rightHeight ? leftHeight +1: rightHeight +1;}private:// 左单旋 2 1 newNode 练成线,单纯的右边高voidRotateL(Node* parent){ _rotateCount++;// 测试旋转次数if(parent ==nullptr|| parent->_right ==nullptr)return; Node* curNode = parent->_right; Node* curLeft = curNode->_left;// curLeft 有可能为空// 先处理 curNode 的 left 结点,curLeft 有可能是空 parent->_right = curLeft;if(curLeft) curLeft->_parent = parent;// 再处理 curNode 结点// parent 有可能是根节点,也有可能是子树的根节点if(parent == _root){// 先立新根 _root = curNode; curNode->_parent =nullptr;// 再挂旧根 parent->_parent = curNode; curNode->_left = parent;}else{ Node* ppNode = parent->_parent;// 这里不知道 parent 是 ppNode 的 左孩子 还是 右孩子 if(parent == ppNode->_left) ppNode->_left = curNode;else ppNode->_right = curNode; curNode->_parent = ppNode;// 挂 parent parent->_parent = curNode; curNode->_left = parent;}}// 右单旋 -2 -1 newNode 连成线,单纯的左边高voidRotateR(Node* parent){ _rotateCount++;// 测试旋转次数// parent 为空 或 curNode 为空的情况if(parent ==nullptr|| parent->_left ==nullptr)return; Node* curNode = parent->_left; Node* curRight = curNode->_right;// 把 curNode 的 right 给给 parent 的 left parent->_left = curRight;if(curRight) curRight->_parent = parent;if(parent == _root){// 先立新根 _root = curNode; curNode->_parent =nullptr;// 再挂旧根 curNode->_right = parent; parent->_parent = curNode;}else{ Node* ppNode = parent->_parent;// 找 parent 是 ppNode 的左还是右if(parent == ppNode->_left) ppNode->_left = curNode;else ppNode->_right = curNode; curNode->_parent = ppNode;// 挂 parent curNode->_right = parent; parent->_parent = curNode;}}};intmain(){constint N =10000000; vector<int> v; v.reserve(N);srand(time(0));for(size_t i =0; i < N; i++){ v.push_back(rand()+ i);// 插入随机数据//v.push_back(i); // 插入有序数据} RBTree<int,int> rbt;for(auto e : v){ rbt.insert(make_pair(e, e));//cout << "Insert:" << e << "->" << t.isBalance() << endl;} cout <<"红黑树是否平衡: "<< rbt.isBalance()<< endl; cout <<"红黑树的高度: "<<rbt.Height()<< endl; cout <<"红黑树的旋转次数: "<< rbt._rotateCount << endl; cout << endl << endl; AVLTree<int,int> avlt;for(auto e : v){ avlt.insert(make_pair(e, e));//cout << "Insert:" << e << "->" << t.IsBalance() << endl;} cout <<"AVL树是否平衡: "<< avlt.isBalance()<< endl; cout <<"AVL树的高度: "<< avlt.height()<< endl; cout <<"AVL树的旋转次数: "<< avlt._rotateCount << endl;return0;}

8. 结语

红黑树作为一种“近似平衡”的二叉搜索树,通过颜色标记旋转调整,在插入与删除操作频繁的场景下有效避免了 AVL 树的高频旋转问题。
它在时间复杂度上与 AVL 树相同,都是 O ( log ⁡ N ) O(\log N) O(logN),但实际应用中更具工程价值,因此成为了 STL mapsetmultimapmultiset 等容器的底层数据结构

本文自底向上实现了红黑树的插入、变色与旋转机制,并通过与 AVL 树的对比,揭示了红黑树在平衡效率与维护开销上的折中之美。

✅ 红黑树的核心思想:不追求完美平衡,而是保证最长路径不超过最短路径的 2 倍;通过简单的局部旋转与变色操作实现全局近似平衡;用颜色规则取代复杂的高度平衡因子,使得结构更简洁、效率更高。

红黑树的实现也标志着我们正式跨入了 C++ STL 底层机制探索的第二阶段
下一步,我们将基于该结构实现 mapset 等容器,深入理解 迭代器、模板参数与仿函数 在标准库中的实际应用。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!
你的每一次互动,都是对作者最大的鼓励!征程尚未结束,让我们在广阔的世界里继续前行! 🚀

Read more

C++socket网络编程——udp服务器

C++socket网络编程——udp服务器

目录 一.端口号 VS  PID 二.套接字编程的类型 三.socket编程接口 四.基于udp的服务端和客户端全部代码 客户端 服务端 五.解释与运行 一些细节: 六.总结 一.端口号 VS  PID pid已经能够标识一台主机上的一个唯一一个进程了,为什么还需要端口号? 1. 不是所有的进程都需要网络通信,但是所有的进程都需要都pid; 2. 系统和网络功能解耦。         另外,一个进程可以绑定多个端口,但一个端口只能被一个进程绑定。         系统内定的端口号【0,1023】一般都要有固定的应用层协议使用,如http:80,https:443。 二.套接字编程的类型 1. 域间套接字编程——同一个机器内 2. 原始套接字编程——网络工具 3. 网络套接字编程—

By Ne0inhk
【C++哲学】面向对象的三大特性之 继承

【C++哲学】面向对象的三大特性之 继承

🔥拾Ծ光:个人主页 👏👏👏欢迎来到我的专栏:《C++》,《数据结构》,《C语言》 目录 一、继承的概念和定义 1、什么是继承? 2、继承的定义 2.1、定义格式 2.2、继承基类成员访问方式的变化 3、继承类模板 二、基类和派生类 1、基类与派生类间的类型转换⭐️ 2、继承中的作用域 2.1、隐藏规则 2.2、面试题实践⭐️ 三、派生类的默认成员函数 1、派生类4个常见的默认成员函数⭐️⭐️ 2、实现一个不能被继承的类 四、继承与友元 五、继承与静态成员 六、多继承及其菱形继承问题 1、继承模型 2、虚继承 3、

By Ne0inhk

Qt C++ 插件开发指南:插件架构设计与动态加载实战

一、Qt插件开发概述 1.1 插件技术核心价值 插件架构是一种软件设计模式,通过将应用程序的核心功能与扩展功能分离,允许第三方或开发者在不修改主程序源代码的情况下对软件进行功能扩展、特性升级或定制化改造。这种架构模式在大型软件系统中应用广泛,典型场景包括:IDE工具的插件扩展(如Qt Creator的插件体系)、图形软件的滤镜插件、办公软件的功能模块扩展等。 Qt作为成熟的C++跨平台框架,提供了一套完整的插件机制,其核心优势体现在: * 跨平台兼容性:Qt插件可在Windows、Linux、macOS等系统上统一构建和加载,无需针对不同平台编写适配代码; * 二进制级扩展:插件以动态链接库(.dll/.so/.dylib)形式存在,主程序与插件通过统一接口交互,无需重新编译主程序即可更新插件; * 低耦合设计:主程序仅依赖插件接口抽象,不关心具体实现,插件可独立开发、测试和部署; * 框架原生支持:Qt提供QPluginLoader、QObject、Q_INTERFACES等核心类和宏,简化插件注册、发现和通信流程。 1.2 Qt插件的两种类型 Qt插件体系主要分

By Ne0inhk