红黑树
- 1.红黑树的概念
- 2.红黑树的规则
- 3.红黑树的实现
- 1.红黑树的结构
- 2.红黑树的插入
- 1.情况一:变色
- 2.情况二:单旋 + 变色
- 3.情况三:双旋 + 变色
- 3.红黑树的查找
- 4.红黑树的验证
- 5.红黑树的删除
- 6.红黑树与AVL树的性能比较
- 4.总代码
- 1.RBTree.h
- 2.Test.cpp
1.红黑树的概念
- 红黑树是一棵⼆叉搜索树,他的每个结点增加⼀个存储位来表示结点的颜色,可以是红色或者黑色。
- 通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。
2.红黑树的规则
- 每个结点不是红色就是黑色。
- 根结点是黑色的。
- 如果一个结点是红色的,若该红色结点存在孩子,则它的孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点,但是可以存在连续的黑色结点。
- 对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点。
如下4幅红黑树图所示:
说明:《算法导论》等书籍上补充了一条每个叶子结点(NIL)都是黑色的规则。这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,了解这个概念即可。如下图所示:
思考:红黑树如何确保最长路径不超过最短路径的2倍的?
- 由规则4可知,从根到NULL结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径:全是黑色结点的路径。假设最短路径的长度为bh(black height)
- 由规则2和规则3可知,任意一条路径不会有连续的红色结点,所以极端场景下,最长路径:一黑一红间隔组成。那么最长路径的长度为2*bh
- 综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都存在的。假设任意一条从根到NULL结点路径的长度为x,那么 bh <= h <= 2*bh
红黑树的效率:
- 假设N是红黑树树中结点数量,h最短路径的长度,那么
2^h - 1 <= N < 2^(2*h) - 1
,由此推出 h ≈ logN,也就是意味着红黑树增删查改最坏也就是走最长路径 2*logN,那么时间复杂度还是O(logN)
红黑树的表达相对AVL树还要抽象一些,AVL树通过高度差直观的控制了平衡。红黑树通过4条规则的颜色约束,间接的实现了近似平衡,它们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为它对平衡的控制没那么严格。
3.红黑树的实现
1.红黑树的结构
//枚举值表示颜色
enum Colour
{RED,BLACK
};//默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{//控制平衡也要加入parent指针 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){}
};template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;//using Node = RBTreeNode<K, V>;public://...private:Node* _root = nullptr;
};
2.红黑树的插入
红黑树插入一个值的大致过程:
- 插入一个值按⼆叉搜索树规则进行插入,插入后我们只需要观察是否符合红黑树的4条规则。
- 如果是空树插入,新增结点是黑色结点。如果是非空树插入,新增结点必须红色结点,因为非空树插入,新增黑色结点就破坏了规则4,规则4是很难维护的。
- 非空树插入后,新增结点必须红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束。
- 非空树插入后,新增结点必须红色结点,如果父亲结点是红色的,则违反规则3。进一步分析,c是红色,p为红,g必为黑,这三个颜色都固定了,关键的变化看u的情况,需要根据u分为以下几种情况分别处理。
说明:下图中假设我们把新增结点标识为c(cur),c的父亲标识为p(parent),p的父亲标识为g(grandfather),p的兄弟标识为u(uncle)
1.情况一:变色
c为红色,p为红色,g为黑色。u存在且为红色,则将p和u变成黑色,g变成红色。再把g当做新的c,继续找p、u、g往上更新。
分析:因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加一个黑色结点,g再变红,相
当于保持g所在子树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新
是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束
了;如果g就是整棵树的根,再把g变回黑色。
方法:只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。
给出以下3幅图观察变色过程:
图1将以上类似的处理进行了抽象表达,d/e/f代表每条路径拥有hb个黑色结点的字树,a/b代表每条路径拥有hb-1个黑色结点的根为红的子树,hb>=0
- 图1如下:
图2/图3/图4,分别展示了 hb=0/hb=1/hb=2 的具体情况组合分析,当hb等于2时,这里组合情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理方式一样的,变色再继续往上处理即可,所以我们只需要看抽象图即可。
- 图2如下:
- 图3如下:
- 图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的父亲是黑色还是红色或者空都不违反规则。
- 场景一:u不存在
- 场景二:u存在且为黑
如果p是g的右孩子,c是p的右孩子,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。
- 场景一:u不存在
- 场景二:u存在且为黑
抽象图如下:
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的父亲是黑色还是红色或者空都不违反规则。
- 场景一:u不存在
- 场景二:u存在且为黑
如果p是g的左孩子,c是p的右孩子,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。
- 场景一:u不存在
- 场景二:u存在且为黑
抽象图如下:
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){// g// p uNode* uncle = grandfather->_right;if (uncle && uncle->_col == RED) //叔叔存在且为红{parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}else //叔叔不存在或者叔叔存在且为黑{if (parent->_left == cur) //g右单旋{// g// p u// cRotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else //p左单旋、g右单旋{// g// p u// cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}else{// g// u pNode* uncle = grandfather->_left;if (uncle && uncle->_col == RED){uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}else{if (parent->_right == cur){// g// u p// cRotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// g// u p// cRotateR(parent);RotateL(grandfather);cur->_col = BLACK;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;//记录parent的父节点Node* pParent = parent->_parent;subL->_right = parent;parent->_parent = subL;//当parent是根节点时if (parent == _root){_root = subL;subL->_parent = nullptr;}else //当parent不是根节点时{subL->_parent = pParent;if (pParent->_left == parent)pParent->_left = subL;elsepParent->_right = subL;}
}//左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;//记录parent的父节点Node* pParent = parent->_parent;subR->_left = parent;parent->_parent = subR;//当parent是根节点时if (parent == _root){_root = subR;subR->_parent = nullptr;}else //当parent不是根节点时{subR->_parent = pParent;if (pParent->_left == parent)pParent->_left = subR;elsepParent->_right = subR;}
}
3.红黑树的查找
按⼆叉搜索树逻辑实现即可,搜索效率为O(logN)
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;
}
4.红黑树的验证
这里获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,一定能保证最长路径不超过最短路径的2倍。
- 规则1:枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
- 规则2:直接检查根即可。
- 规则3:前序遍历,遇到红色结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。
- 规则4:前序遍历,遍历过程中用形参记录跟到当前结点的blackNum(黑色结点数量),前序遍历遇到黑色结点就++blackNum,走到空就计算出了一条路径的黑色结点数量。再任意一条路径黑色结点数量作为参考值,依次比较即可。
bool Check(Node* root, int blackNum, const int refNum)
{if (root == nullptr){//前序遍历走到空时,意味着一条路径走完了 //cout << blackNum << endl;if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}//检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了 if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);
}bool IsRBTree()
{if (_root == nullptr)return true;if (_root->_col == RED)return false;//计算其中一条路径上黑色结点的个数作为参考值int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);
}
void TestRBTree()
{RBTree<int, int> t;//常规的测试用例 //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };//特殊的带有双旋场景的测试用例 int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (auto e : a){t.Insert({ e, e });}t.InOrder();cout << t.IsRBTree();
}
5.红黑树的删除
等待更新…
6.红黑树与AVL树的性能比较
AVL树的代码可以看上一篇博客
void TestTree2()
{const int N = 10000000;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; i++){v.push_back(rand() + i);}size_t begin1 = clock();AVLTree<int, int> t;for (auto e : v){t.Insert(make_pair(e, e));}size_t end1 = clock();size_t begin2 = clock();RBTree<int, int> rbt;for (auto e : v){rbt.Insert(make_pair(e, e));}size_t end2 = clock();cout << "AVL Insert:" << end1 - begin1 << endl;cout << "RB Insert:" << end2 - begin2 << endl;cout <<"AVL IsBalance:"<< t.IsAVLTree() << endl;cout << "RB IsBalance:" << rbt.IsRBTree() << endl;cout << "AVL Height:" << t.Height() << endl;cout << "RB Height:" << rbt.Height() << endl;cout << "AVL Size:" << t.Size() << endl;cout << "RB Size:" << rbt.Size() << endl;size_t begin3 = clock();//确定在的值for (auto e : v){t.Find(e);}//随机值/*for (size_t i = 0; i < N; i++){t.Find((rand() + i));}*/size_t end3 = clock();cout << "AVL Find:" << end3 - begin3 << endl;size_t begin4 = clock();// 确定在的值for (auto e : v){rbt.Find(e);}// 随机值/*for (size_t i = 0; i < N; i++){t.Find((rand() + i));}*/size_t end4 = clock();cout << "RB Find:" << end4 - begin4 << endl;
}
总结:性能具体差不多
4.总代码
1.RBTree.h
#pragma once//枚举值表示颜色
enum Colour
{RED,BLACK
};//默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{//控制平衡也要加入parent指针 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){}
};template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;//using Node = RBTreeNode<K, V>;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){// g// p uNode* uncle = grandfather->_right;if (uncle && uncle->_col == RED) //叔叔存在且为红{parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}else //叔叔不存在或者叔叔存在且为黑{if (parent->_left == cur) //g右单旋{// g// p u// cRotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else //p左单旋、g右单旋{// g// p u// cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}else{// g// u pNode* uncle = grandfather->_left;if (uncle && uncle->_col == RED){uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}else{if (parent->_right == cur){// g// u p// cRotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// g// u p// cRotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK; //根节点必须是黑色return true;}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;}//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;//记录parent的父节点Node* pParent = parent->_parent;subL->_right = parent;parent->_parent = subL;//当parent是根节点时if (parent == _root){_root = subL;subL->_parent = nullptr;}else //当parent不是根节点时{subL->_parent = pParent;if (pParent->_left == parent)pParent->_left = subL;elsepParent->_right = subL;}}//左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;//记录parent的父节点Node* pParent = parent->_parent;subR->_left = parent;parent->_parent = subR;//当parent是根节点时if (parent == _root){_root = subR;subR->_parent = nullptr;}else //当parent不是根节点时{subR->_parent = pParent;if (pParent->_left == parent)pParent->_left = subR;elsepParent->_right = subR;}}void InOrder(){_InOrder(_root);cout << endl;}int Height(){return _Height(_root);}int Size(){return _Size(_root);}bool Check(Node* root, int blackNum, const int refNum){if (root == nullptr){//前序遍历走到空时,意味着一条路径走完了 //cout << blackNum << endl;if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}//检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了 if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);}bool IsRBTree(){if (_root == nullptr)return true;if (_root->_col == RED)return false;//计算其中一条路径上黑色结点的个数作为参考值int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);}private:void _InOrder(Node* root){if (root == nullptr) return;_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}int _Height(Node* root){if (root == nullptr) return 0;int leftHelght = _Height(root->_left);int rightHelght = _Height(root->_right);return max(leftHelght, rightHelght) + 1;}int _Size(Node* root){if (root == nullptr) return 0;return _Size(root->_left) + _Size(root->_right) + 1;}private:Node* _root = nullptr;
};
2.Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
#include<vector>
using namespace std;#include"RBTree.h"
#include"AVLTree.h"void TestTree1()
{RBTree<int, int> t;//常规的测试用例 //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };//特殊的带有双旋场景的测试用例 int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (auto e : a){t.Insert({ e, e });}t.InOrder();cout << t.IsRBTree();
}void TestTree2()
{const int N = 10000000;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; i++){v.push_back(rand() + i);}size_t begin1 = clock();AVLTree<int, int> t;for (auto e : v){t.Insert(make_pair(e, e));}size_t end1 = clock();size_t begin2 = clock();RBTree<int, int> rbt;for (auto e : v){rbt.Insert(make_pair(e, e));}size_t end2 = clock();cout << "AVL Insert:" << end1 - begin1 << endl;cout << "RB Insert:" << end2 - begin2 << endl;cout <<"AVL IsBalance:"<< t.IsAVLTree() << endl;cout << "RB IsBalance:" << rbt.IsRBTree() << endl;cout << "AVL Height:" << t.Height() << endl;cout << "RB Height:" << rbt.Height() << endl;cout << "AVL Size:" << t.Size() << endl;cout << "RB Size:" << rbt.Size() << endl;size_t begin3 = clock();//确定在的值for (auto e : v){t.Find(e);}//随机值/*for (size_t i = 0; i < N; i++){t.Find((rand() + i));}*/size_t end3 = clock();cout << "AVL Find:" << end3 - begin3 << endl;size_t begin4 = clock();// 确定在的值for (auto e : v){rbt.Find(e);}// 随机值/*for (size_t i = 0; i < N; i++){t.Find((rand() + i));}*/size_t end4 = clock();cout << "RB Find:" << end4 - begin4 << endl;
}int main()
{TestTree1();TestTree2();return 0;
}