跳到主要内容
C++ 红黑树原理与实现:变色旋转及完整代码 | 极客日志
C++ 算法
C++ 红黑树原理与实现:变色旋转及完整代码 红黑树是一种自平衡二叉搜索树,通过颜色约束确保最长路径不超过最短路径的两倍。核心内容包括红黑树的四条规则,插入操作的三种处理情况:变色、单旋加变色、双旋加变色。提供完整的 C++ 代码实现,涵盖节点结构定义、插入逻辑、左右旋函数、查找、Size 及高度计算接口设计,以及验证红黑树合法性的方法。相比 AVL 树,红黑树在插入时旋转次数更少,效率更高,广泛应用于 STL map/set 等容器底层。
极客工坊 发布于 2026/3/29 更新于 2026/4/23 1 浏览C++ 红黑树原理与实现
1 初识红黑树:概念熟悉
红黑树也是一棵二叉搜索树 ,其每个结点会增加一个存储位(颜色存储位),用来表示结点的颜色(两种颜色),可以是红色或者黑色(因此被称为红黑树)。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束 ,红黑树确保没有一条路径会比其他路径长出 2 倍,因而是接近平衡的,也就是说,红黑树是近似平衡的。
2 了解红黑树规则
2.1 红黑树的四条规则
2.1.1 红黑树规则
2.1.2 结合图示,体会红黑树规则
我们观察几张红黑树的图大家就能有更进一步的体会了——
2.1.3 结合图例,理解红黑树的路径数量问题:NIL
上面的红黑树里面有几条路径?实际上有 6 条路径。
有人会问,这个 NIL 是啥?这个叫叶子节点,但是这个和我们二叉树里面的叶子结点不是同一个东西!这个叶子结点指的是空节点。有些书籍上也把 NIL 叫做外部结点。NIL 是为了方便准确的标识出所有路径。
说明:《算法导论》等书籍上补充了一条每个叶子结点(NIL)都是黑色的规则。它这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点。
2.2 红黑树是怎么确保最长路径不超过最短路径的 2 倍的?
由规则 4 可知,从根到 NULL 结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径就是全是黑色结点的路径,假设最短路径长度为 bh(blackheight)。
由规则 2 和规则 3 可知,任意一条路径不会有连续的红色结点,所以极端场景下,最长的路径就是一黑一红间隔组成,那么最长路径的长度为 2*bh。
综合红黑树的 4 点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都所存在的。假设任意一条从根到 NULL 结点路径的长度为 x,那么 bh <= h <= 2*bh。
2.3 理解红黑树的效率 假设 N 是红黑树树中结点数量,h 最短路径的长度,那么 2^h - 1 <= N < 2^(2 * h) - 1,由此推出 h ≈ logN,也就意味着红黑树增删查改最坏情况也就是走最长路径 2*logN,那么时间复杂度还是 O(logN)。
红黑树的表达相对 AVL 树要抽象一些,AVL 树的直观在于通过高度差控制了平衡,而红黑树是通过四条规则的颜色约束来间接实现近似平衡,他们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为红黑树对平衡的控制没那么严格,所以我们比较常用的还是红黑树,因为红黑树的旋转次数更少,效率还不比 AVL 树低 ,因此实践中红黑树用得比 AVL 树多,这也是红黑树 相比 AVL 树的优势 。
3 实现红黑树
3.1 红黑树的结构实现
3.2 红黑树的插入问题以及插入的代码实现
3.2.1 红黑树中插入一个值的大致过程
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.2.2 三种情况
3.2.3 情况 1:变色 c 为红,p 为红,g 为黑,u 存在且为红,则将 p 和 u 变黑,g 变红。在把 g 当做新的 c,继续往上更新。
分析:因为 p 和 u 都是红色,g 是黑色,把 p 和 u 变黑,左边子树路径各增加一个黑色结点,g 再变红,相当于保持 g 所在子树的黑色结点的数量不变,同时解决了 c 和 p 连续红色结点的问题,需要继续往上更新是因为,g 是红色,如果 g 的父亲还是红色,那么就还需要继续处理;如果 g 的父亲是黑色,则处理结束了;如果 g 就是整棵树的根,再把 g 变回黑色。
情况 1 只变色,不旋转 。所以无论 c 是 p 的左还是右,p 是 g 的左还是右,都是上面的变色处理方式。
跟 AVL 树类似,上图向我们展示了一种具体情况,但是实际中需要这样处理的有很多种情况。
下图将以上类似的处理进行了抽象表达,d / e / f 代表每条路径拥有 hb 个黑色结点的子树,a / b 代表每条路径拥有 hb - 1 个黑色结点的根为红的子树,hb >= 0。
下图则分别展示了 hb == 0 / hb == 1 / hb == 2 的具体情况组合分析——
当 hb 等于 2 时,这里组合情况上百亿种,这些样例是帮助我们理解,不论情况多少种,有多么复杂,处理方式都是一样的,变色再继续往上处理即可,所以我们只需要看抽象图即可。
3.2.4 情况 2:单旋 + 变色 c 为红,p 为红,g 为黑,u 不存在或者 u 存在且为黑,u 不存在,则 c 一定是新增结点,u 存在且为黑,则 c 一定不是新增,c 之前是黑色的,是在 c 的子树中插入,符合情况 1,变色将 c 从黑色变成红色,更新上来的。
分析:p 必须变黑,才能解决,连续红色结点的问题,u 不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转 + 变色。
如果 p 是 g 的左,c 是 p 的左,那么以 g 为旋转点进行右单旋,再把 p 变黑,g 变红即可。p 变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 p 的父亲是黑色还是红色或者空都不违反规则。
如果 p 是 g 的右,c 是 p 的右,那么以 g 为旋转点进行左单旋,再把 p 变黑,g 变红即可。p 变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 p 的父亲是黑色还是红色或者空都不违反规则。
3.2.5 情况 3:双旋 + 变色 c 为红,p 为红,g 为黑,u 不存在或者 u 存在且为黑,u 不存在,则 c 一定是新增结点,u 存在且为黑,则 c 一定不是新增,c 之前是黑色的,是在 c 的子树中插入,符合情况 1,变色将 c 从黑色变成红色,更新上来的。
分析:p 必须变黑,才能解决,连续红色结点的问题,u 不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转 + 变色。
如果 p 是 g 的左,c 是 p 的右,那么先以 p 为旋转点进行左单旋,再以 g 为旋转点进行右单旋,再把 c 变黑,g 变红即可。c 变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 c 的父亲是黑色还是红色或者空都不违反规则。
如果 p 是 g 的右,c 是 p 的左,那么先以 p 为旋转点进行右单旋,再以 g 为旋转点进行左单旋,再把 c 变黑,g 变红即可。c 变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 c 的父亲是黑色还是红色或者空都不违反规则。
3.2.6 红黑树插入的代码实现
3.3 红黑树的旋转
3.3.1 右旋
3.3.2 左旋
3.3.3 左右双旋
3.3.4 右左双旋
3.4 红黑树的查找问题 红黑树的查找就按搜索二叉树的逻辑来实现就可以了,时间复杂度是 O(logn)。
3.5 Size() 还是同样的问题,这里传根不能直接传,得从外部调用内部的函数完成传根——
3.5.1 公共接口:不需要传 root public : int Size () { return _Size(_root); }
int Height () { return _Height(_root); }
3.5.2 私有实现:必须传 root private : int _Size(Node* root) {
if (root == nullptr ) return 0 ;
return _Size(root->_left) + _Size(root->_right) + 1 ;
}
3.5.3 解决方案:双重接口设计
3.5.3.1 公共层(面向用户)
tree.Size ();
tree.Height ();
tree.IsBalanceTree ();
3.5.3.2 私有层(内部递归)
_Size(root);
_Height(root);
_IsBalanceTree(root);
3.5.4 为什么要设计成双重接口?
tree.Size (someNode);
tree._Size(tree._root);
3.5.5 总结
3.5.6 Size() 双重接口设计实现
3.6 Height()
3.7 IsBalanceTree()
3.8 中序遍历:InOrder()
3.9 验证红黑树 这里获取最长路径和最短路径,检查最长路径不超过最短路径的 2 倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查 4 点规则,满足这 4 点规则,一定能保证最长路径不超过最短路径的 2 倍。
1、规则 1 枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
2、规则 2 直接检查根即可。
3、规则 3 前序遍历检查,遇到红色结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了,即孩子不一定存在,父亲一定存在。
4、规则 4 前序遍历,遍历过程中用形参记录根节点到当前结点的 blackNum(黑色结点数量),前序遍历遇到黑色结点就++blackNum,走到空就计算出了一条路径的黑色结点数量。再任意一条路径黑色结点数量作为参考值,依次比较即可。
3.10 红黑树的删除 和 AVL 树那里一样,红黑树这里的删除我们也不做讲解——
有兴趣的可以去看一下参考资料,书的话推荐看一下《算法导论》或者《STL 源码剖析》,这两本书里面会有讲解。
完整代码示例与实践演示
RBTree.h: #pragma once
#include <iostream>
using namespace std;
#include <assert.h>
enum Color { BLACK, RED };
template <class K ,class V >
struct RBTreeNode {
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Color _col;
RBTreeNode (const pair<K, V>& kv) :_kv(kv)
, _left(nullptr )
, _right(nullptr )
, _parent(nullptr )
, _col(RED) {}
};
template <class K ,class V >
struct RBTree {
typedef RBTreeNode<K, V> Node;
public :
bool 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);
cur->_col = RED;
if (parent->_kv.first < kv.first) {
parent->_right = cur;
} else {
parent->_left = cur;
}
cur->_parent = parent;
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
if (grandfather->_left == parent) {
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) {
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
if (cur == parent->_left) {
RotateR (grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
} else {
RotateL (parent);
RotateR (grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break ;
}
} else {
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) {
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
if (cur == parent->_right) {
RotateL (grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
} else {
RotateR (parent);
RotateL (grandfather);
grandfather->_col = RED;
}
break ;
}
}
}
_root->_col = BLACK;
return true ;
}
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 (parent == _root) {
_root = subL;
subL->_parent = nullptr ;
} else {
if (parentParent->_left == parent) {
parentParent->_left = subL;
} else {
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
void RotateL (Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (parentParent == nullptr ) {
_root = subR;
subR->_parent = nullptr ;
} else {
if (parent == parentParent->_left) {
parentParent->_left = subR;
} else {
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
}
void InOrder () {
_InOrder(_root);
cout << endl;
}
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 ;
}
bool IsBalanceTree () {
if (_root && _root->_col == RED) return false ;
int left_bn = 0 ;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK) left_bn++;
cur = cur->_left;
}
return _Checkcolor(_root, 0 , left_bn);
}
int Height () {
return _Height(_root);
}
int Size () {
return _Size(_root);
}
private :
int _Size(Node* root) {
if (root == nullptr ) return 0 ;
return _Size(root->_left) + _Size(root->_right) + 1 ;
}
int _Height(Node* root) {
if (root == nullptr ) return 0 ;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1 ;
}
bool _Checkcolor(Node* root, int root_cur_bn,const int left_bn) {
if (root == nullptr ) {
if (root_cur_bn != left_bn) {
cout << "黑色节点的数量不相等" << endl;
return false ;
}
return true ;
}
if (root->_col == BLACK) {
root_cur_bn++;
}
if (root->_col == RED && root->_parent && root->_parent->_col == RED) {
cout << root->_kv.first << "存在连续的红色节点" << endl;
return false ;
}
return _Checkcolor(root->_left, root_cur_bn, left_bn) && _Checkcolor(root->_right, root_cur_bn, left_bn);
}
int _InOrder(Node* root) {
if (root == nullptr ) return 0 ;
_InOrder(root->_left);
cout << root->_kv.first << " " ;
_InOrder(root->_right);
}
private :
Node* _root = nullptr ;
};
Tree.cpp: #define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
using namespace std;
#include "RBTree.h"
void TestRBTree1 () {
RBTree<int , int >t;
int a[] = { 4 ,2 ,6 ,1 ,3 ,5 ,15 ,7 ,16 ,14 };
for (auto e : a) {
if (e == 7 ) {
int i = 0 ;
}
t.Insert ({ e,e });
cout << "Insert:" << e << "->" ;
t.InOrder ();
}
t.InOrder ();
}
void TestRBTree2 () {
const int N = 1000000 ;
vector<int > v;
v.reserve (N);
srand (time (0 ));
for (size_t i = 0 ; i < N; i++) {
v.push_back (rand () + i);
}
size_t begin2 = clock ();
RBTree<int , int > t;
for (auto e : v) {
t.Insert (make_pair (e, e));
}
size_t end2 = clock ();
cout << "Insert:" << end2 - begin2 << endl;
cout << t.IsBalanceTree () << endl;
cout << "Height:" << t.Height () << endl;
cout << "Size:" << t.Size () << endl;
size_t begin1 = clock ();
for (size_t i = 0 ; i < N; i++) {
t.Find ((rand () + i));
}
size_t end1 = clock ();
cout << "Find:" << end1 - begin1 << endl;
}
int main () {
TestRBTree2 ();
return 0 ;
}
运行结果
TestRBTree1()
TestRBTree2() 相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online