数据结构进阶——红黑树
数据结构进阶——红黑树
- 1. 红黑树的概念
- 2. 红黑树的性质
- 3. 红黑树节点的定义
- 4. 红黑树的插入
- 5. 红黑树的验证
- 6. 红黑树的删除
- 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结构的红黑树。
// 颜色定义
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进行右单旋,转化为情况三(右右黑),再根据情况三进行处理。 总结一下就是,先进行右左双旋,再将
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// cRotateR(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲右边,左右双旋(左右黑)// g// p// cRotateL(parent);RotateR(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}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// cRotateL(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲左边,右左双旋(右左黑)// g// p// cRotateR(parent);RotateL(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}}// 不管前面如何处理,最后都要把根节点变黑_root->_col = BLACK;return true;}
}
5. 红黑树的验证
红黑树的检测分为两步:
- 检测其是否满足二叉搜索树(中序遍历是否为有序序列);
- 检测其是否满足红黑树的性质。
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. 完整代码
#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// cRotateR(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲右边,左右双旋(左右黑)// g// p// cRotateL(parent);RotateR(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}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// cRotateL(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲左边,右左双旋(左右黑)// g// p// cRotateR(parent);RotateL(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}}// 不管前面如何处理,最后都要把根节点变黑_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(){_Size(_root);}
};
2. 测试Insert和Find的效率
#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:
Find Success!!!
IsBalance: 1
Heigh: 27
Insert cost: 0.304353 s
Find cost: 0.00027 s
- 可以看到插入和查找效率非常高。
