目录
前言
红黑树的概念及性质
红黑树的效率
红黑树的结构
红黑树的插入
变色不旋转
单旋+变色
双旋+变色
插入代码如下所示:
红黑树的查找
红黑树的验证
红黑树代码如下所示:
小结
前言
在前面的文章我们介绍了AVL这一棵完全二叉搜索树,我们分析后发现AVL树查找的时间复杂度是O(logN),说明这是一个很优秀的数据结构,但是在底层实现我们发现,AVL树为了维持它的性质,是需要进行频繁的旋转的,这样会造成不必要的消耗,那么有没有一种数据结构既可保证它的时间复杂度为O(logN),又能避免频繁的旋转呢?这就是我们今天要介绍的红黑树,学习了红黑树之后我们将用红黑树作为底层,去封装实现一个模拟的map和set,下面让我们一起来学习吧。
参考文章:
据结构(五)——AVL树(平衡二叉搜索树)
红黑树的概念及性质
首先我们要知道什么是红黑树,与AVL树相比,红黑树的节点多了一个标志符号,这个标志符号表示节点的颜色,我们这里用枚举来表示,通过控制节点的颜色来判平衡以及判断是否需要旋转。由于红黑树的旋转不像AVL树那么频繁,所以它是一颗不完全平衡二叉搜索树。
下面我们引入路径的概念,我们把从根节点开始,一直到空节点的一条路线称为路径
它的规则如下所示:
规则一:根节点必须是黑色,其余节点的颜色不是黑色就是红色
规则二:每个红色节点的左右子树必须是红色的,即:不存在连续的红色节点
规则三:每条路径的黑色节点数量是相同的,其中我们把空节点视为黑色节点
规则四:最长路径的节点数量不超过最长路径节点数量的二倍
分析上面的规则我们发现,其中最关键也是最难维护的就是规则三,我们发现,在极端场景下,最短路径就是全为黑色节点的路径,最长路径就是红黑间隔的路径,所以维护好了规则三,规则四自然就实现了
红黑树的效率
由于有规则四的存在,我们设红黑树最短路径的节点数量是h,那么最长路径的节点数量就是2*h,我们设每条路径上黑色节点的数量是N,那么他们应该满足:,也就是说,如果我们要在红黑树当中进行增删查改,最少要走的长度是
,查找的次数就是log(N),最坏也就是查找2*log(N),分析下来,它的时间复杂度还是log(N)。
红黑树的结构
从上面的分析中我们知道,红黑树的大致框架与AVL树相似,只不过将平衡因子改成了用枚举表示的颜色,后续我们也是通过控制颜色来判平衡,因此红黑树的结构如下所示:
enum Colour
{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;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;
public:
private:Node* _root = nullptr;
};
红黑树的插入
我们来分析红黑树的插入过程,首先由于它是一颗搜索树,所以按照搜索树的规则进行插入,更改插入结点的颜色,然后我们向上进行调整。
对于插入节点的颜色,我们发现,如果我们插入黑色,那么就在当前路径上增加了一个黑色节点,不符合每条路径上黑色节点数量相同这一规则,所以我们插入节点的颜色应该为红色。代码如下所示:
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{cout << "插入值已经存在,请重新插入" << endl;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;
}
当我们插入红色节点后,会出下面几种情况:
情况一:父节点为黑色,不违反红黑树的规则,那么就停止向上调整
情况二:父节点为红色,违反规则,那么就需要向上调整
接下来我们重点分析情况二。由于不能有两个连续的红色节点,所以我们应当把父节点变成黑色,但是将父节点变成黑色后我们就在当前路径上凭空增加了一个黑色节点,这样破坏了黑色节点数量相同这一规则,那么我们就需要调整其他路径上黑色节点的数量,如果是这样的话,是不符合我们的预期的,我们再仔细观察红黑树的结构,我们把父节点的父节点定义为grandfather(祖父节点),那么祖父节点的另一个子节点称为uncle(叔节点),我们发现,无论什么情况,只要涉及到需要向上调整,祖父节点总是黑色的,此时,如果叔节点存在且为红色的话,我们就只需要将父节点和叔节点颜色变为黑色,再将祖父节点变为红色,这样就解决了这个问题;如果叔节点不存在或者存在且为空节点,那么此时我们就需要做特殊处理。
通过上述分析,我们总结一下红黑树的插入过程:
第一步:插入节点。按照二叉搜索树的规则插入节点,链接父节点,将插入节点的颜色改成红色,如果是根节点,就变为黑色。
第二步:向上调整黑色节点的数量,让其满足红黑树的规则。
接下来我们逐步来分析向上调整。
根据上面的分析我们知道,我们可以根据uncle节点的有无以及颜色,可以分为下面两种情况:
情况一:uncle节点存在且颜色为红色
情况二:uncle节点存在且为黑色或者uncle不存在
如下图所示:
对于情况一,我们只需要将父节点和uncle节点颜色变为黑色,然后再将祖父节点颜色变为红色,然后向上更新parent节点和grandfather节点,因此我们需要一个循环,循环结束的条件是:parent为空或者parent的颜色为黑色。
对于情况二,我们发现这种情况又需要分为两种场景,一种是插入节点是parent的左节点,另一种是插入节点是parent的右节点,这两种情景无论我们怎么变色都没有办法满足红黑树的规则,为此我们需要寻找一种新的解决办法。
回到红黑树的定义,我们知道,红黑树是一颗不完全平衡二叉搜索树,从前面的AVL树我们知道,要成为一颗平衡树,我们要做的就是旋转,而旋转分为左旋和右旋,那么我们尝试将AVL树的旋转带入红黑树当中,那么对于parent与cur都位于同一侧节点的这种情况,我们使用单旋,对于parent与cur位于不同一侧节点的这种情况,我们使用双旋。
综上所述,红黑树的插入可以分为以下三种不同的情况:
变色不旋转
条件:grandfather为黑,parent与cur为红色,uncle存在且为红。
解决办法:parent与cur变为黑色,grandfather变为红色,然后再将grandfather当成新的cur,继续向上更新。
分析:由于cur与parent都是红色,所以我们应该把parent变为黑色,这样就在当前节点凭空增加了一个黑色节点,从而导致黑色节点失衡;由于uncle是红色,为了让黑色节点数量保持平衡,所以我们需要把uncle也变成黑色,然后让grandfather变为红色。
单旋+变色
条件:grandfather为黑色,parent与cur为红色,uncle不存在或者uncle存在且为黑,并且parent与cur位于同一侧
解决办法:以grandfather节点为旋转点,如果位于左侧则右旋;如果位于右侧则左旋,旋转结束后让parent变为黑色,grandfather变成红色
分析:由于uncle是黑色节点,如果我们直接按照第一种情况进行变色,那么就会导致uncle这条路径上始终少一个节点,这样导致节点失衡,所以我们要先进行旋转,旋转结束后parent成为了cur与grandfather的父节点,为了让黑色节点数量保持平衡,我们需要让parent变成黑色,让grandfather变成红色
双旋+变色
条件:grandfather为黑色,parent与cur为红色,uncle不存在或者uncle存在且为黑,并且parent与cur位于不同侧
解决办法:首先以parent为旋转点进行一次旋转,然后再以grandfather为旋转点进行一次旋转,旋转结束后让cur变成黑色,让grandfather变成红色。
分析:由于uncle是黑节点,cur与parent位于相反的两侧,如果我们旋转一次,结果还是这种情况,所以我们需要进行两次旋转,旋转结束后,为了保持黑色节点数量的平衡,我们需要将cur变成黑色,让grandfather变成红色
插入代码如下所示:
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{cout << "插入值已经存在,请重新插入" << endl;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 (parent->_left == cur){RotateR(grandfather);grandfather->_col = RED;parent->_col = BLACK;}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 (parent->_right == cur){RotateL(grandfather);grandfather->_col = RED;parent->_col = BLACK;}else{RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;return true;
}
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;subR->_left = parent;if (subRL)subRL->_parent = parent;Node* grandfather = parent->_parent;parent->_parent = subR;if (_root == parent){_root = subR;subR->_parent = nullptr;}else{if (grandfather->_left == parent){grandfather->_left = subR;}else{grandfather->_right = subR;}subR->_parent = grandfather;}}
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;subL->_right = parent;if (subLR)subLR->_parent = parent;Node* grandfather = parent->_parent;parent->_parent = subL;if (_root == parent){_root = subL;subL->_parent = nullptr;}else{if (grandfather->_left == parent){grandfather->_left = subL;}else{grandfather->_right = subL;}subL->_parent = grandfather;}
}
旋转逻辑在这篇文章介绍:
数据结构(五)——AVL树(平衡二叉搜索树)
红黑树的查找
红黑树的查找与二叉搜索树相同,都是通过遍历比较节点值与目标值的大小知道找到目标值所在的节点然后返回或者直到遍历完这颗树为止,其代码如下所示:
Node* Find(const pair<K, V>& kv)
{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 cur;}}cout << "目标值不存在" << endl;return nullptr;
}
红黑树的验证
前面我们验证红黑树时使用的方法是验证平衡因子以及左右子树高度差,实际上我们是按照AVL树的规则进行验证的,那么对于红黑树而言我们要通过它的规则进行验证,检查最长路径不超过最短路径的二倍吗?这样显然是不现实的,那么我们是通过什么方法去验证呢?我们还是通过递归去验证,不同的是我们引入了一个新的参数——参考值,这个值是用来记录从根节点到空节点这一整条路径中黑色节点的数量的,我们用这个值作为参考去比较其他路径上黑色节点的数量是否与这个值相同。其代码如下所示:
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 IsBalance()
{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);
}
红黑树代码如下所示:
enum Colour
{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;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;
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{cout << "插入值已经存在,请重新插入" << endl;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 (parent->_left == cur){RotateR(grandfather);grandfather->_col = RED;parent->_col = BLACK;}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 (parent->_right == cur){RotateL(grandfather);grandfather->_col = RED;parent->_col = BLACK;}else{RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;return true;}void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;subR->_left = parent;if (subRL)subRL->_parent = parent;Node* grandfather = parent->_parent;parent->_parent = subR;if (_root == parent){_root = subR;subR->_parent = nullptr;}else{if (grandfather->_left == parent){grandfather->_left = subR;}else{grandfather->_right = subR;}subR->_parent = grandfather;}}void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;subL->_right = parent;if (subLR)subLR->_parent = parent;Node* grandfather = parent->_parent;parent->_parent = subL;if (_root == parent){_root = subL;subL->_parent = nullptr;}else{if (grandfather->_left == parent){grandfather->_left = subL;}else{grandfather->_right = subL;}subL->_parent = grandfather;}}Node* Find(const pair<K, V>& kv){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 cur;}}cout << "目标值不存在" << endl;return nullptr;}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 IsBalance(){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;
};
小结
本篇文章我们着重介绍了红黑树的概念以及插入逻辑的实现,红黑树作为一颗不完全平衡二叉搜索树有着独特的地方,它是通过节点的颜色来判断是否平衡以及是否需要旋转操作,它省去了频繁旋转所带来的消耗,从而更加追求效率,总的来说,红黑树是一颗非常优秀的数据结构
以上就是本篇博客的主要内容,如果对您有所帮助的话,记得点赞、评论、关注加转发,您的支持就是我创作的最大动力