红黑树及其简单实现
目录
前言
一、红黑树的概念
1.1 红黑树的规则
1.2 红黑树如何保证没有一条路径会超过其他路径长度的两倍
1.3 红黑树的效率
二、红黑树的实现
2.1 红黑树的结构
2.2 红黑树的插入
2.2.1 插入的大致架构
2.2.2 确保平衡的两种情况
情况一:变色
情况二:旋转加变色
单旋+变色
双旋+变色
2.3 红黑树的简单实现
前言
终于到了红黑树部分了,我们之前用的 map 和 set 都是使用红黑树封装的,让我们来看看红黑树到底是个什么东西吧。
一、红黑树的概念
红黑树是一棵二叉搜索树,他的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。
1.1 红黑树的规则
红黑树的规则:
1、每个结点一定是红色或黑色的。
2、红黑树的根节点一定是黑色的。
3、任意一条路径不能出现连续的红色节点(可以出现连续黑色)。
4、任意一条路径(根节点到null算一条路径)的黑色节点数量都与其他路径黑色节点数量相同。
有的同学可能在课本上见过这样的红黑树:
《算法导论》等书籍上补充了一条每个叶子结点(NIL)都是黑色的规则。他这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了 方便准确的标识出所有路径。但是没有实际用途,可以忽略。
其次就是路径数量问题,大家可以看看下面这棵树有几条路径:
是不是有人会说 4 条路径,其实算树的路径我们必须从根节点算到空结点才算是一条路径,所以我们要把上面的图片空结点画上,再数路径数量,如下:
此时再数是不是就有 9 条路径了,哈哈哈。
1.2 红黑树如何保证没有一条路径会超过其他路径长度的两倍
其实,红黑树保证没有一条路径会超过其他路径的两倍是由其规则决定的,首先如果每条路径都有2个黑色节点,那一条路径长度最短的时候不就是连续两个黑色节点,不存在红节点的时候嘛,此时一条路径长度最短,为2。那什么时候长度最长呢,当然就是黑红相间的时候,因为红色不能连续,所以两个黑色节点之间只能有一个红色节点,此时就达到了路径最长长度 4 。
所以一条路径的长度一定是介于 h(black)~ 2h(black)之间的。这就保证了没有一条路径会超过其他路径长度的两倍。
1.3 红黑树的效率
设节点数量为 N,红黑树最短路径的高为 h (也就是只有黑色节点的一条路径长度为 h),如果所有路径长度都跟最短路径高度一样,那么节点数量为 N = 2^h - 1 。如果最短路径是 h,但是其他路径都是红黑节点相间,此时其他路径高度都为 2h,由于最短路径高度只有 h,所以此红黑树达不到满二叉树的地步,所以 N < 2^(2h) - 1。综上所述,2^h - 1 <= N < 2^(2h) - 1,所以 h ≈ logN,那么时间复杂度就是 O(logN)。
到这里大家很明显能看出来红黑树控制平衡远不如AVL树控制平衡能力强,但是为什么红黑树用的更广泛呢,其实是因为控制平衡能力越强,每次插入节点就会导致更容易发生旋转,所以有优点也必定有缺点。而且两者效率差距也不大,查找时红黑树最多也就比AVL树多找一倍的高度,而二叉搜索树高度很小就可以存储数量很大的数据,所以几乎不会影响效率。
二、红黑树的实现
2.1 红黑树的结构
红黑树的结构跟 AVL树基本相同,我们只是把平衡因子换成了颜色,别的基本没有变化。
enum Color
{BLACK,RED
};template<class K, class V>
struct RBTreeNode
{pair<K, V> _kv;RBTreeNode<K, V>* _parent;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;Color _col;RBTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr), _left(nullptr), _right(nullptr){ }
};
2.2 红黑树的插入
2.2.1 插入的大致架构
1、插入一个值时需要按照二叉搜索树的规则进行插入,插入后需要看是否满足红黑树的三条规则。
2、如果是空树插入节点,那么插入的结点必须是黑色结点;如果是非空树插入结点,插入节点必须是红色节点,因为如果插入的是黑色结点就会违反每条路径黑色节点数量一致的规则。
3、非空树插入新的红色节点后,如果父节点是黑色节点,此时未违反红黑树规则,插入结束。
4、非空树插入新的红色节点后,如果父节点是红色节点,此时违反了红黑树不能存在连续红色节点的规则。我们需要进一步分析,分析如下:
此时新插入节点 cur(后面用 c 代替)为红,插入节点的父节点parent(后面用 p 代替)为红,那么爷爷grandfather(后面用 g 代替)一定存在且为黑色(爷爷一定存在因为如果爷爷不存在,根节点就是父亲,而父亲是红色,红黑树根节点不允许是红色;一定为黑色是因为爷爷也是红色的话,父亲爷爷就两个红色了,没插入之前就不是红黑树了),此时我们就需要看叔叔uncle(g 的另一个儿子 ,后面用 u 代替)的具体情况:
2.2.2 确保平衡的两种情况
情况一:变色
c 为红,p 为红,g 为黑,若 u 存在且为红,则 g 变红,c 和 p 变黑,再把 g 当作新的 c,继续往上更新即可。g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束了;如果g就是整棵树的根,直接把g变为黑色即可。如下图只需要变色一次即可:
上面是具体情况,跟AVL树一样,实际有很多种情况,我们看一下泛型:
情况二:旋转加变色
c 为红,p 为红,g 为黑,若 u 不存在或 u 存在且为黑,则需要进行旋转和变色。
我们先来分析一下情况:如果 u 不存在,则 c 一定是新增节点;而 u 存在且为黑, 则 c 一定不是新增节点。为什么呢,我给大家画一下:
至于旋转的规则还是跟之前相同,只不过不需要修改平衡因子了。
单旋+变色
当是下面两种情况时是单旋+变色:
左边的需要右单旋,并将 g 变为红色,p 变成黑色(此时p为根结点),右边需要左单旋,p 变黑,g 变红。
双旋+变色
当时下面两种情况时需要双旋+变色:
左边的情况需要进行左右双旋,最后 c 变为根节点且变为黑色,g 变为红色(至于是怎么旋转的去看上一节,还是很详细的);右边的情况需要进行右左双旋,此时 c 变为根节点且变为黑色,g变为红色。
2.3 红黑树的简单实现
ok,最后附上红黑树的简单实现,注释很详细,大家感兴趣的可以自己去看:
RBTree.h
#pragma once#include <iostream>
#include <vector>
using namespace std;enum Color
{BLACK,RED
};template<class K, class V>
struct RBTreeNode
{pair<K, V> _kv;RBTreeNode<K, V>* _parent;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;Color _col;RBTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr), _left(nullptr), _right(nullptr){ }
};template<class K, class V>
class 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* cur = _root;Node* parent = nullptr;while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if(cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else{return false;}}cur = new Node(kv);cur->_col = RED;//记得插入节点的颜色,一定要插入红色,不能插入黑色,//否则会导致每个路径的黑色节点数量不等,但是两个连续的红节点就很好修改了if (kv.first < parent->_kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;//若插入节点的父亲是黑色,则插入结束if (parent->_col == BLACK){return true;}//如果插入节点父亲是红色,则需要看叔叔的情况//首先分析一下情况:爷爷一定存在,因为如果爷爷不存在,根节点就是父亲,而父亲是红色,红黑树根节点不允许是红色//爷爷一定是黑色,因为爷爷也是红色的话,父亲爷爷就两个红色了,没插入之前就不是红黑树了while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (parent == grandfather->_left)//如果父亲是爷爷的左,那么叔叔就是父亲的右{Node* uncle = grandfather->_right;//情况一:叔叔存在且为红,变色if (uncle && uncle->_col == RED){grandfather->_col = RED;//爷爷变红,父亲叔叔变黑parent->_col = BLACK;uncle->_col = BLACK;cur = grandfather;//继续向上改变parent = cur->_parent;}else//情况二:叔叔不存在或叔叔为黑,变色加旋转{// g// p u//c//cur是parent的左子树时,单旋if (cur == parent->_left){RotateR(grandfather);//右单旋grandfather->_col = RED;//grandfather变红,parent变黑parent->_col = BLACK;}// g// p u// c//当cur是parent的右孩子时,左右双旋,//这里没有平衡因子调节,所以不需要单独搞一个双旋了else{RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;//和AVL树一样,旋转完后就平衡了,直接break就ok了}}else//如果父亲是爷爷的右,那么叔叔就是爷爷的左{Node* uncle = grandfather->_left;//情况一:uncle存在且为红if (uncle && uncle->_col == RED){grandfather->_col = RED;parent->_col = BLACK;uncle->_col = BLACK;cur = grandfather;parent = cur->_parent;}//情况二:uncle不存在或uncle存在且为黑else{// g// u p// c//cur是parent的右子树时,左单旋if (cur == parent->_right){RotateL(grandfather);grandfather->_col = RED;parent->_col = BLACK;}// g// u p// c//cur是parent的左子树时,右左双旋else{RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;//根必须是黑色的,并且就算根是红色的我们把根改为红色也不影响每条路径黑色结点的数量return true;}//旋转还是AVL树那样,只不过不用管平衡因子了void RotateR(Node* parent)//传的参数还是要旋转的子树或树的根节点{Node* subL = parent->_left;//parent的左孩子Node* subLR = parent->_left->_right;//parent左孩子的右子树parent->_left = subLR;//把parent左孩子的右子树放在parent的左子树上subL->_right = parent;//parent变为subL的右子树if (subLR)//如果subLR不为空才能解引用{subLR->_parent = parent;//改变subLR的父节点}Node* parentParent = parent->_parent;//将parent的父节点存储下来subL->_parent = parentParent;//将subL的父节点指向parent的父节点(即使parent是整棵树的根节点,也就是parentParent为空)parent->_parent = subL;//将parent的父节点指向subLif (_root == parent)//判断原来的parent是子树的根节点还是整棵树的根节点{_root = subL;}else//若是子树,修改子树根节点的父节点的指向{if (parentParent->_left == parent){parentParent->_left = subL;}else{parentParent->_right = subL;}}}void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;subR->_left = parent;if (subRL){subRL->_parent = parent;}Node* parentParent = parent->_parent;subR->_parent = parentParent;parent->_parent = subR;if (_root == parent){_root = subR;}else{if (parentParent->_left == parent){parentParent->_left = subR;}else{parentParent->_right = subR;}}}void InOrder(){_InOrder(_root);cout << endl;}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}//检查是否满足红黑树的特点,使用前序遍历bool Check(Node* root, int blacknum, const int refnum)//第二个参数是存储每条路径黑色节点数量,第三个参数是存储第一条路经黑色节点数量,//用作参考值,比较每条路径黑色节点数量是否和第一条路径相同{if (root == nullptr)//根节点为空,有可能是树是空;也有可能已经遍历完一条路径,遍历到空节点了{if (blacknum != refnum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}//检查红节点的父亲是不是红色节点,如果是就违反了规则if (root->_col == RED && root->_parent && 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);}//检查是不是一棵红黑树(检察树是否满足红黑树平衡条件),通过调用Check函数检查bool IsBalanceTree(){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:Node* _root = nullptr;
};
test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "RBTree.h"// 测试代码
void TestRBTree1()
{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.IsBalanceTree() << endl;
}void TestRBTree2()
{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);}RBTree<int, int> t;for (size_t i = 0; i < v.size(); ++i){t.Insert(make_pair(v[i], v[i]));}cout << t.IsBalanceTree() << endl;
}int main()
{//TestRBTree1();TestRBTree2();return 0;
}
后记
ok宝贝们,到这里红黑树基本实现就结束了,后面我们会用将这个代码直接改造成 map 和 set(当然是简单版本,哈哈哈),希望大家跟我一块努力,加油💪。