数据结构进阶------红黑树
- [1. 红黑树的概念](#1. 红黑树的概念)
- [2. 红黑树的性质](#2. 红黑树的性质)
- [3. 红黑树节点的定义](#3. 红黑树节点的定义)
- [4. 红黑树的插入](#4. 红黑树的插入)
- [5. 红黑树的验证](#5. 红黑树的验证)
- [6. 红黑树的删除](#6. 红黑树的删除)
- [7. 红黑树完整代码+测试](#7. 红黑树完整代码+测试)
1. 红黑树的概念
红黑树 ,是一种二叉搜索树 ,但在每个结点上增加一个存储位表示结点的颜色 ,可以是Red或Black 。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍 ,因而是接近平衡的。

2. 红黑树的性质
- 1. 每个结点不是红色就是黑色
- 2. 根节点是黑色的
- 3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点,是红黑树特有的NIL节点)
其中,第5条性质,是为了帮大家更好的理解性质4,举个例子:下面这棵树符合红黑树的定义吗?

很显然,性质1,2,3都是符合的,但是性质4符合吗?所有路径上,是否有相同数量的黑色节点?是不是大眼一看感觉也符合性质4,别急,下面我们把NIL节点也带上:

可以看到,用蓝色线条画出的这条路径 ,是不是很容易被忽略?从根节点向下看 ,其他路径上的黑色节点数都是2个(不算NIL节点),唯独蓝色线路上的黑色节点只有1个,所以上面这棵树是不符合红黑树定义的,它不是红黑树!
满足了如上5条性质,就可以保证,最长路径不超过最短路径的两倍,但是这是为什么?感兴趣的同学可以下去思考一下,只能说,这是天才的设计,很抽象,从学习者的角度来讲,只要能使用并控制红黑树的结构,这就够了,没必要向天才看齐。
3. 红黑树节点的定义
下面我们先实现一颗
kv结构的红黑树。
cpp
// 颜色定义
enum Colour
{
RED,
BLACK
};
// 红黑树节点
template<class K, class V>
struct RBTreeNode
{
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)
{}
};
4. 红黑树的插入
1. 思考,如果我们要新插入一个节点,这个节点应该是红色还是黑色?
-
先假设我们插入黑色节点,这会导致什么?会导致一整条路径上的黑色节点数量+1,这代价是不是太大了,我们要进行很多调整结构的操作,使得树重新满足性质4。
-
如果我们插入红色节点呢?
- 如果新节点的父节点是黑色的 ,那么就结束了 ,不用做什么其他调整,就算插入成功了 ,因为这完全不会影响树的整体结构。

- 如果新节点的父节点是红色 ,那么为了满足性质3(红色节点的子节点必须是黑色 ),就需要做出一些调整。

- 如果新节点的父节点是黑色的 ,那么就结束了 ,不用做什么其他调整,就算插入成功了 ,因为这完全不会影响树的整体结构。
-
单单分析到这,就发现,插入红色节点,要比插入黑色节点代价小的多,很多情况下竟然只需要变色,就可以使红黑树结构平衡,而不用调整节点间的父子关系(旋转)。
2. 确定了新节点必须是红色节点,接下来就是分情况讨论
-
如果新节点的双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;
-
但当新插入节点的双亲节点颜色为红色 时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论;
-
约定:
cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点(cur不一定是新插入节点,也可能是调整上去的)。

-
如果一颗子树需要被调整,那么至少有三个节点的颜色,我们是确定的 。1. cur当前节点为红色。2. cur的父节点一定是红色。3. cur的爷爷节点,也就是父节点的父节点,一定是黑色。 因为调整之前,以
cur为根节点的子树的上一级树必然是符合规则的,如果不符合,还调整什么?这棵树就是有问题的。 -
所以接下来,我们讨论的实际上是叔叔节点不同颜色or存在/不存在的情况 + cur在p的左节点or右节点的情况 + p在g的左节点or右节点的情况(叔叔节点也有可能不存在)。

- 将上面这些情况全部综合起来,一共有 4 * 4 = 16 种情况需要讨论 。u节点存在or不存在or红色or黑色,共四种 X 上图中四种节点位置状态,共四种。
- 有同学可能会想,如果
u节点是黑色,那么整个树就不满足规则4了啊,平衡被打破了啊。所以u节点要么是红色,要么不存在。这就是没有考虑到,cur可能不是新插入节点,可能是原来是黑色的,只是调整的过程中,由黑变红了。这种情况下,整棵树的黑节点数量还是平衡的。
下面的讨论中有一些情况会合并成一种情况(好编程),最终呈现出来,会小于16种。但是我们需要知道,本质上,就是对这16种情况进行处理。
2. 开始讨论
-
情况一(叔叔红):
-
情况1.1:
p为g的左节点,cur为p的左节点,u存在且为红(左左红)。- 解决方案:将
p,u改为黑,g改为红,然后把g当成cur,继续向上调整。

- 解决方案:将
-
情况1.2:
p为g的右节点,cur为p的右节点,u存在且为红(右右红)。- 解决方案和情况一完全相同,这里不画图了,大家对着情况一的图,自行脑补一下。
-
情况1.3:
p为g的左节点,cur为p的右节点,u存在且为红(左右红)。- 解决方案和情况一完全相同。
-
情况1.4:
p为g的右节点,cur为p的左节点,u存在且为红(右左红)。- 解决方案和情况一完全相同。
-
将上面所有的情况
1.*,总结为一种,即叔叔为红色(叔叔红),统一解决方案:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
-
-
情况二(左左黑):
p为g的左节点,cur为p的左节点,u不存在/u存在且为黑。- 解决方案:先对
g节点进行右单旋,然后将p,g变色--p变黑,g变红。

- 解决方案:先对
-
情况三(右右黑):
p为g的右节点,cur为p的右节点,u不存在/u存在且为黑。- 解决方案: 与情况三相比,唯一的不同是,需要对
g进行左单旋 。变色部分完全一样,将p,g变色--p变黑,g变红。
- 解决方案: 与情况三相比,唯一的不同是,需要对
-
情况四(左右黑):
p为g的左节点,cur为p的右节点,u不存在/u存在且为黑。- 解决方案:先对p进行左单旋,转化为情况二(左左黑),再根据情况二进行处理。 总结一下就是,先左右双旋,再将
cur变黑,g变红。

- 解决方案:先对p进行左单旋,转化为情况二(左左黑),再根据情况二进行处理。 总结一下就是,先左右双旋,再将
-
情况五(右左黑):
p为g的右节点,cur为p的左节点,u存在且为红。- 解决方案:先对p进行右单旋,转化为情况三(右右黑),再根据情况三进行处理。 总结一下就是,先进行右左双旋,再将
cur变黑,g变红。
- 解决方案:先对p进行右单旋,转化为情况三(右右黑),再根据情况三进行处理。 总结一下就是,先进行右左双旋,再将
cpp
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
private:
Node* _root = nullptr;
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 (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
// 设定新增节点为红色
cur = new Node(kv);
cur->_col = RED;
if (kv.first > parent->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 如果父节点存在并且是红色,再处理,是黑色不用处理
while(parent && parent->_col == RED)
{
Node* grandFather = parent->_parent;
if (parent == grandFather->_left)
{
// 这里只是说父亲在爷爷左边,而cur有可能在父亲的左节点也有可能在右节点
// 后面还要再做讨论
// g
// p u
// c
// 父亲是爷爷的左节点,那么叔叔就是爷爷的右节点
Node* uncle = grandFather->_right;
// 叔叔存在,并且叔叔是红色(叔叔红)
if (uncle && uncle->_col == RED)
{
// 变色
uncle->_col = parent->_col = BLACK;
grandFather->_col = RED;
// 继续往上处理
cur = grandFather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// cur 在父亲左边,右单旋(左左黑)
// g
// p
// c
RotateR(grandFather);
parent->_col = BLACK;
grandFather->_col = RED;
}
else
{
// cur 在父亲右边,左右双旋(左右黑)
// g
// p
// c
RotateL(parent);
RotateR(grandFather);
grandFather->_col = RED;
cur->_col = BLACK;
}
// 旋转之后直接就平衡了,直接break
break;
}
}
else // parent == grandFather->_right
{
// 这里只是说父亲在爷爷右边,而cur有可能在父亲的左节点也有可能在右节点
// 后面还要再做讨论
// g
// u p
// c
// 父亲是爷爷的右节点,那么叔叔就是左节点
Node* uncle = grandFather->_left;
// 叔叔存在,并且叔叔是红色(叔叔红)
if (uncle && uncle->_col == RED)
{
// 变色
uncle->_col = parent->_col = BLACK;
grandFather->_col = RED;
// 继续向上处理
cur = grandFather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
// cur 在父亲右边,左单旋(右右黑)
// g
// p
// c
RotateL(grandFather);
parent->_col = BLACK;
grandFather->_col = RED;
}
else
{
// cur 在父亲左边,右左双旋(右左黑)
// g
// p
// c
RotateR(parent);
RotateL(grandFather);
grandFather->_col = RED;
cur->_col = BLACK;
}
// 旋转之后直接就平衡了,直接break
break;
}
}
}
// 不管前面如何处理,最后都要把根节点变黑
_root->_col = BLACK;
return true;
}
}
5. 红黑树的验证
红黑树的检测分为两步:
- 检测其是否满足二叉搜索树(中序遍历是否为有序序列);
- 检测其是否满足红黑树的性质。
cpp
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
private:
Node* _root = nullptr;
public:
...
// 中序遍历
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
// 检查
// 1. 红色节点的子节点是否为黑色节点
// 2. 根节点->当前节点这条路径的黑色节点的数量
bool Check(Node* root, int blacknum, const int refVal)
{
if (root == nullptr)
{
if (blacknum != refVal)
{
cout << "存在黑色节点数量不相等的路径" << endl;
return false;
}
return true;
}
// 子节点情况太多了,我们直接反向检查父节点,只要红色节点的父节点不是红色,就是符合要求的
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "有连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
blacknum++;
}
return Check(root->_left, blacknum, refVal)
&& Check(root->_right, blacknum, refVal);
}
// 看看红黑树是否符合要求
bool IsBalance()
{
return _IsBalance(_root);
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
return true;
if (root->_col == RED)
return false;
// 参考值,算出最左路径的黑节点数量
int refVal = 0;
Node* cur = root;
while (cur) {
if (cur->_col == BLACK)
refVal++;
cur = cur->_left;
}
int blacknum = 0;
return Check(root, blacknum, refVal);
}
};
6. 红黑树的删除
红黑树的删除本节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
7. 红黑树完整代码+测试
1. 完整代码
cpp
#pragma once
#include <iostream>
enum Colour
{
RED,
BLACK
};
using namespace std;
template<class K, class V>
struct RBTreeNode
{
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<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
private:
Node* _root = nullptr;
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 (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
// 设定新增节点为红色
cur = new Node(kv);
cur->_col = RED;
if (kv.first > parent->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 如果父节点存在并且是红色,再处理,是黑色不用处理
while(parent && parent->_col == RED)
{
Node* grandFather = parent->_parent;
if (parent == grandFather->_left)
{
// 这里只是说父亲在爷爷左边,而cur有可能在父亲的左节点也有可能在右节点
// 后面还要再做讨论
// g
// p u
// c
// 父亲是爷爷的左节点,那么叔叔就是爷爷的右节点
Node* uncle = grandFather->_right;
// 叔叔存在,并且叔叔是红色(叔叔红)
if (uncle && uncle->_col == RED)
{
// 变色
uncle->_col = parent->_col = BLACK;
grandFather->_col = RED;
// 继续往上处理
cur = grandFather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// cur 在父亲左边,右单旋(左左黑)
// g
// p
// c
RotateR(grandFather);
parent->_col = BLACK;
grandFather->_col = RED;
}
else
{
// cur 在父亲右边,左右双旋(左右黑)
// g
// p
// c
RotateL(parent);
RotateR(grandFather);
grandFather->_col = RED;
cur->_col = BLACK;
}
// 旋转之后直接就平衡了,直接break
break;
}
}
else // parent == grandFather->_right
{
// 这里只是说父亲在爷爷右边,而cur有可能在父亲的左节点也有可能在右节点
// 后面还要再做讨论
// g
// u p
// c
// 父亲是爷爷的右节点,那么叔叔就是左节点
Node* uncle = grandFather->_left;
// 叔叔存在,并且叔叔是红色(叔叔红)
if (uncle && uncle->_col == RED)
{
// 变色
uncle->_col = parent->_col = BLACK;
grandFather->_col = RED;
// 继续向上处理
cur = grandFather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
// cur 在父亲右边,左单旋(右左黑)
// g
// p
// c
RotateL(grandFather);
parent->_col = BLACK;
grandFather->_col = RED;
}
else
{
// cur 在父亲左边,右左双旋(左右黑)
// g
// p
// c
RotateR(parent);
RotateL(grandFather);
grandFather->_col = RED;
cur->_col = BLACK;
}
// 旋转之后直接就平衡了,直接break
break;
}
}
}
// 不管前面如何处理,最后都要把根节点变黑
_root->_col = BLACK;
return true;
}
// 左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 更新左右节点
parent->_right = subRL;
subR->_left = parent;
Node* parentParent = parent->_parent;
// 更新父节点
parent->_parent = subR;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
// 将子树链接进整体
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else if (parentParent->_left == parent)
{
parentParent->_left = subR;
subR->_parent = parentParent;
}
else
{
parentParent->_right = subR;
subR->_parent = parentParent;
}
}
// 右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 更新左右节点
parent->_left = subLR;
subL->_right = parent;
Node* parentParent = parent->_parent;
// 更新父节点
parent->_parent = subL;
if (subLR != nullptr)
{
subLR->_parent = parent;
}
// 将子树链接进整体
if (_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
else if (parentParent->_left == parent)
{
parentParent->_left = subL;
subL->_parent = parentParent;
}
else
{
parentParent->_right = subL;
subL->_parent = parentParent;
}
}
// 中序遍历
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
// 检查
// 1. 红色节点的子节点是否为黑色节点
// 2. 根节点->当前节点这条路径的黑色节点的数量
bool Check(Node* root, int blacknum, const int refVal)
{
if (root == nullptr)
{
if (blacknum != refVal)
{
cout << "存在黑色节点数量不相等的路径" << endl;
return false;
}
return true;
}
// 子节点情况太多了,我们直接反向检查父节点,只要红色节点的父节点不是红色,就是符合要求的
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "有连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
blacknum++;
}
return Check(root->_left, blacknum, refVal)
&& Check(root->_right, blacknum, refVal);
}
// 看看红黑树是否符合要求
bool IsBalance()
{
return _IsBalance(_root);
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
return true;
if (root->_col == RED)
return false;
// 参考值,算出最左路径的黑节点数量
int refVal = 0;
Node* cur = root;
while (cur) {
if (cur->_col == BLACK)
refVal++;
cur = cur->_left;
}
int blacknum = 0;
return Check(root, blacknum, refVal);
}
bool 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 true; // 找到了,返回真
}
}
return false; // 找不到,返回假
}
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;
}
int Height()
{
return _Height(_root);
}
int _Size(Node* root)
{
if (root == nullptr)
{
return 0;
}
return _Size(root->_left) + _Size(_root->_right) + 1;
}
int Size()
{
return _Size(_root);
}
};
2. 测试Insert和Find的效率
cpp
#include <iostream>
#include <chrono>
#include <vector>
using namespace std;
#include "RBTree.h"
// 小范围测试
void Test1()
{
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
RBTree<int, int> t;
for (auto e : a)
{
t.Insert(make_pair(e, e));
}
t.InOrder();
cout << "IsBalance: " << t.IsBalance() << endl;
}
// 大范围测试
void Test2()
{
const int N = 100 * 10000; // 测试100w条数据的插入
vector<int> v;
v.reserve(N);
srand((unsigned int)time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand() + (int)i);
}
RBTree<int, int> t;
// 测试插入耗时
auto start1 = chrono::high_resolution_clock::now();
for (auto e : v)
{
t.Insert(make_pair(e, e));
}
auto end1 = chrono::high_resolution_clock::now();
double insertTime = chrono::duration_cast<chrono::microseconds>(end1 - start1).count() / 1e6;
// 测试查找耗时
auto start2 = chrono::high_resolution_clock::now();
if (t.Find(v[100])) cout << "Find Success!!!" << endl;
auto end2 = chrono::high_resolution_clock::now();
double findTime = chrono::duration_cast<chrono::microseconds>(end2 - start2).count() / 1e6;
cout << "IsBalance: " << t.IsBalance() << endl;
cout << "Heigh: " << t.Height() << endl;
cout << "Insert cost: " << insertTime << " s" << endl;
cout << "Find cost: " << findTime << " s" << endl;
}
int main()
{
Test2();
return 0;
}
Test2大范围测试的结果,数据量100w:
0
Find Success!!!
IsBalance: 1
Heigh: 27
Insert cost: 0.304353 s
Find cost: 0.00027 s
- 可以看到插入和查找效率非常高。