首先呢,在开头这里我给出我自己写的数据结构,里面包含了常见的数据结构,可以给大家提供参考
但是这里我gitee下面的代码可能由于我在迭代全新版本的实现,这里的代码可能出现一些问题,如果有问题,可以私信我修改
DataStructure: 这是我写数据结构的库,主要效仿的是stl中的基本结构 - Gitee.com
注意了呀!!! 我这篇文章里面的代码片段如果你直接ctrl + c + ctrl + v肯定跑不了,如果想要原码还是去上面链接的gitee仓库里面的RBTree.h里面的代码考下来才是正确的
红黑树简介
红黑树(Red-Black Tree)是一种自平衡的二叉查找树,其定义和性质如下:
- 每个节点要么是红色,要么是黑色。这是红黑树的基本属性之一。
- 根节点是黑色。这意味着在所有情况下,根节点的颜色都是黑色。
- 每个叶子节点(NIL)是黑色。这里的叶子节点指的是空节点或称为NIL节点。
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。这保证了树的平衡性。
- 没有两个相邻的红色节点。即任何红色节点的子节点必须是黑色。
这些性质共同确保了红黑树在插入、删除等操作时能够保持相对平衡的状态,从而提供高效的查找性能。红黑树的时间复杂度为O(log n),使其成为一种非常实用的数据结构
在我的上一篇博客中,我介绍了AVL树这种类似于红黑树的数据结构,相对于AVL树,红黑树的满足条件没有那么苛刻,这也决定了其在时间复杂度上有着更大的优势,所以这里就红黑树我提出一下我的看法。
红黑树中节点的定义:
效仿AVL树的结构,红黑树如其名,我们应该在他的节点中定义一个全新的元素color(颜色),通过这里元素,我们可以设计建立红黑树的结构,还是那句话,任何的引入的全新的元素会给我们带来方便,我们在维护这个元素的时候,也会出现痛苦.
enum Color {
Red,
Black,
};
template <class data>
struct RBTreeNode {
RBTreeNode(const data& kv,RBTreeNode<data>* left = nullptr\
,RBTreeNode<data>* right = nullptr,RBTreeNode<data>* parent = nullptr):\
_kv(kv),_left(left),_right(right),_parent(parent)
{}
Color _col = Black;
data _kv;
RBTreeNode<data>* _left;
RBTreeNode<data>* _right;
RBTreeNode<data>* _parent;
};
可能有些书上使用的是int 或者 bool color来标记节点的颜色,但是我还是觉得这里使用枚举使得颜色这一概念更加的形象
红黑树中节点的新增
假如说你之前是学过搜索二叉树或者说你已经了解AVL树,这里中新增的操作对你来说肯定非常的简单(假如你想学红黑树,这里我的建议还是你已经比较了解搜索二叉树的基础上进行学习),这里我们还是通过cur指针进行查找,找到我们应该增加的空指针的位置,并且我们同时设置一个parent指针记录cur指针的父类指针.然后进行增加,到这一步操作为止,其实和普通的搜索二叉树差不多(所以我的建议还是熟练地掌握搜索二叉树才来学习红黑树)当然如果你的天赋足够高,那当我没说.
1.新增节点
由于我的代码是用模版写的比较之间难看懂,所以这里我使用伪代码大概得解释
Node* cur = root;//root是树的根节点
Node* parent = nullptr;//这里parent记录的是cur的parent
//_cmp表示的是我们传递的模版构成的比较的对象
//默认的情况下_cmp = std::less<T>() //这里需要你对模版有一些了解
if(cur == nullptr) root = new RBTreeNode<T>(val);//可以理解吧
while(cur){
if(_cmp(val,cur->_val)) cur = cur->_left;//如果新增的值小于当前的位置,cur继续向左
else if(_cmp(cur->_val,val)) cur = cur->_right;//通过cur向右
else return false;//这种情况就是这个红黑树下面已经存在和val相同的节点,那么也就没有必要新增了
}
//cur这个时候为nullptr
cur = new RBTreeNode<T>(val)
if(_cmp(val,parent->_val) parent->_left = cur;
else parent->_right = cur;
//到这里新增节点结束
2.对二叉树进行平衡操作
为什么需要平衡操作,这是因为我们新增节点的时候肯定默认用的新增一个红色节点(很好理解吧,新增一个节点不得翻天),然后我们新增红节点的时候就会出现一个问题,就是红红连续的情况,比如说,我们在红色父类节点的下面新增一个红色接地,当然这种情况下就显然出现问题了呗.
那么我们怎么解决呢
1.情况一:叔叔存在且为红
这种情况是最简单的这种情况下,我们将爷爷节点变红(爷爷节点原来一定为黑),然后把爸爸和叔叔节点都变黑,然后整理结束,可以return了(对了假如这里爷爷是root节点,还需要将root变回黑(因为root节点一定是黑色的,当然这个操作很简单是吧))
2.叔叔节点不存在或者说叔叔节点为黑
这里就只有两种情况结束
1.红红和爷爷节点非常连续的单边倒
这里这个操作就是说的很悬的右旋操作
2.红红和爷爷是交错的
这样后将新的根变成黑色节点,两个孩子都变成黑色,然后这就是传说中的先左旋后右旋,其实就是把左孩子的右孩子推成根节点,然后变变色就行了,
相对的,右边的情况都是对称的
template<class K, class V,class KOfT, class Compare>
void RBTree<K, V,KOfT, Compare>::RotateLeft(Node *parent) {
//左旋的原理
//由于某个根节点的平衡因子本身处于1的状态,这个时候如果我们在右子树进行push的时候
//平衡被打破,进行右旋,将rightChild的left作为parent的rightchild并且将parent作为
//rightChild的左节点同时将rightchild作为新的根节点
//
//右旋我们要考虑几个关键的点
//1.设置的时候不要忘了我们这里是三叉链的结构
//2.rCLeftChild可能是nullptr
//3.旋转之后,parent 和 rightChild 的 balancefactor都应该变成0
Node* tmp = parent->_parent;//记录根节点
Node* rightChild = parent->_right;
Node* rCLeftChild = rightChild->_left;
parent->_right = rCLeftChild;
rightChild->_left = parent;
parent->_parent = rightChild;
if(rCLeftChild) rCLeftChild->_parent = parent;//由于rCleftChild可能为空
rightChild->_parent = tmp;//他的parent是parent的parent
if(tmp) {
if(parent == tmp->_left) tmp->_left = rightChild;
else tmp->_right = rightChild;
}else _root = rightChild;//这个位置也很重要,更新root节点别忘了
}
template<class K, class V,class KOfT, class Compare>
void RBTree<K, V,KOfT, Compare>::RotateRight(Node *parent) {
Node* tmp = parent->_parent;
Node* leftChild = parent->_left;
Node* lCRightChild = leftChild->_right;
parent->_left = lCRightChild;
leftChild->_right = parent;
parent->_parent = leftChild;
if(lCRightChild) lCRightChild->_parent = parent;
leftChild->_parent = tmp;
if(tmp) {
if(parent == tmp->_left) tmp->_left = leftChild;
else tmp->_right = leftChild;
}else _root = leftChild;
}
}
template <class K,class V,class KOfT,class Compare>
bool RBTree<K,V,KOfT,Compare>::insert(const V& kv) {
enum RBDirct {
left,
right,
};
enum RBDirct rb_dirct = left;
Node* parent = nullptr;
Node* cur = _root;
if(cur == nullptr) {
_root = new RBTreeNode<V>(kv);
_size++;
return true;
}
while(cur) {
parent = cur;
if(_cmp(_koft(kv),_koft(cur->_kv))) {
cur = cur->_left;
rb_dirct = left;
}
else if(_cmp(_koft(cur->_kv),_koft(kv))) {
cur = cur->_right;
rb_dirct = right;
}
else return false;
}
cur = new Node(kv);
_size++;
cur->_col = Red;//新创建节点如果不是根节点,直接给红色的节点
//cur这里是新增的节点
cur->_parent = parent;
if(rb_dirct == left) parent->_left = cur;
else parent->_right = cur;
while(cur) {
parent = cur->_parent;
if(parent == nullptr) {
cur->_col = Black;
break;
}else if(parent->_col == Black) break;
Node* grandParent = parent->_parent;
if(grandParent == nullptr) {
parent->_col = Black;
break;
}else {
Node* uncle = nullptr;
if(parent == grandParent->_left) uncle = grandParent->_right;
else uncle = grandParent->_left;
if(uncle && uncle->_col == Red) {
parent->_col = uncle->_col = Black;
grandParent->_col = Red;
cur = grandParent;
}else {//叔叔不存在 / 叔叔存在且为黑
if(uncle == nullptr) {
if(uncle == grandParent->_left) {
if(cur == parent->_right) {
RotateLeft(grandParent);
grandParent->_col = Red;
parent->_col = Black;
}
else {
RotateRight(parent);
RotateLeft(grandParent);
cur->_col = Black;
grandParent->_col = Red;
}
}
else {
if(cur == parent->_left) {
RotateRight(grandParent);
grandParent->_col = Red;
parent->_col = Black;
}
else {
RotateLeft(parent);
RotateRight(grandParent);
cur->_col = Black;
grandParent->_col = Red;
}
}
}else {
if(uncle == grandParent->_left) {
if(cur == parent->_right) {
RotateLeft(grandParent);
grandParent->_col = Red;
parent->_col = Black;
}
else {
RotateRight(parent);
RotateLeft(grandParent);
cur->_col = Black;
grandParent->_col = parent->_col = Red;
}
}else {
if(cur == parent->_left) {
RotateRight(grandParent);
grandParent->_col = Red;
parent->_col = Black;
}else {
RotateLeft(parent);
RotateRight(grandParent);
cur->_col = Black;
grandParent->_col = parent->_col = Red;
}
}
}
break;
}
}
}
这里的KOfT看不懂也没关系,大概看看实验的底层原理就行
红黑树中节点的删除
红黑树的删除设计到循环的操作,这里谈谈我的较好的理解这个过程的方式,怎么删这里还是简单的用下面的伪代码来解释一下
Node* cur = root;//root是我们树中的根节点
//_cmp默认为std::less<T>()//在c++中什么都可以是类
while(true){
if(cur == nullptr) return false;//没有找到,走到了空节点
else if(_cmp(val,cur->_val)) cur = cur->_left;
else if(_cmp(cur->_val,val))cur = cur->_right;
else break;
}
//找到我们想要删除的节点cur
//如果这里的cur节点有两个孩子节点,我们还是常见的方式,找到后继节点替换
if(cur->_left && cur->_right)//两个孩子节点都存在
{
Node* rightMin = cur->_right;
while(rightMin->_left) rightMin = rightMin->_left;//找到后继节点
std::swap(cur->_val,rightMin->_val);
cur = rightMin;
}
//现在删除cur节点
Node* parent = cur->_parent;
//并且我们需要维护好我们的三叉链的结构
Node* child = nullptr;
if(cur->_left == nullptr) child = cur->_right;
else child = cur->_left;
if(child) child->_parent = parent;
if(parent && parent->_left = cur) parent->_left = child;
else if(parent && parent->_right = cur) parent->_right = child;
delete cur;
cur = child;
这里的cur的节点指到了删除节点的孩子的位置
如果删除的节点是红色
这种情况根本不用讨论,因为他不会影响红黑树的性质
如果删除的节点是黑色
这种时候,我们应该这样理解cur指针,cur指向的是一个缺少了一个黑色的树的根(意思就是这个红黑树出问题部位的根部),这个思想在我们后续进行红黑树的调节的过程中至关重要.bro节点是我们cur的兄弟
bro节点是红色
这种情况非常简单,这种情况下他一定有着两个黑色的孩子节点(他的兄弟黑色个数>=0) (他这一脉肯定>=1),然后我们通过左旋或者右旋这种单旋操作
这样的操作后我们的cur节点不变,但是bro节点变成了黑色,这就不再是bro是红色的情况,就可以再次匹配bro是黑色的情况了
bro是黑色
bro有红色的孩子
如果说bro有着一个红色的孩子,如果他的孩子的方向和他相对于parent的方向相同,就单旋,如果不一样就双选,因为前面我已经写了很多单双旋的操作,所以这里就不在赘述
bro没有红色的孩子(孩子全黑(nullptr也算空))
1.如果parent 是红,ok.将parent变黑,整理结束
2.parent是黑,那我们将bro变红,cur = parent,这个操作是,在现在这级我们已经不能使得红黑树整理好,那我们一不做,二不休,直接让bro也少一个黑色,那么我们这里出现问题红黑树的部分的根就从cur变成了parent,所以这里我们cur = parent这步操作然后我们循环,在上一级也就是parent的parent,parent的bro进行操作,指到根节点,根节点都少一个怎么办,那就整个红黑树少一个呗,这样也不会影响红黑树的基本性质
最后删除的代码大概就是这样
template <class K,class V,class KOfT,class Compare>
bool RBTree<K,V,KOfT,Compare>::erase(const K& key){
mycode::Color colDel = Red;
Node* cur = _root;
while(true){
if(cur == nullptr) return false;
else if(_cmp(key,_koft(cur->_kv))) cur = cur->_left;
else if(_cmp(_koft(cur->_kv),key)) cur = cur->_right;
else break;
}
//这里说明cur就是我们想要删除的节点
//如果有两个孩子节点,那么我们就对其进行找到后继节点的操作
if(cur->_left && cur->_right){
Node* rightMin = cur->_right;
while(rightMin->_left) rightMin = rightMin->_left;
//这个时候就是找到了后继的节点
std::swap(cur->_kv,rightMin->_kv);
cur = rightMin;
}
colDel = cur->_col;//这个记录的删除节点的额颜色
Node* parent = cur->_parent;
Node* child = nullptr;
if(cur->_left == nullptr) child = cur->_right;
else child = cur->_left;
if(parent) {
if(cur == parent->_left) parent->_left = child;
else parent->_right = child;
}else child = _root;//这种情况说明我们删除的节点是根节点
if(child) child->_parent = parent;
delete cur;
cur = child;
//ok删除结束了这个时候我们需要对红黑树进行重构
if(colDel == Red) return true;//这里就是太棒了,删除红节点,直接结束
while(parent){
Node* bro = nullptr;
if(cur == parent->_left) bro = parent->_right;
else bro = parent->_left;
if(bro->_col == Red){
if(bro == parent->_right) RotateLeft(parent);
else RotateRight(parent);
parent->_col = Red;
bro->_col = Black;
}else{//bro->_col == Black
if((bro->_left == nullptr && bro->_right == nullptr) || (bro->_left && bro->_left->_col == Black && bro->_right && bro->_right->_col == Black)){
if(parent->_col == Red){
parent->_col = Black;
bro->_col = Red;
break;
}else {
bro->_col = Red;
cur = parent;
parent = parent->_col;
}
}
if(bro == parent->_right && bro->_right && bro->_right->_col == Red){
RotateLeft(parent);
bro->_col = parent->_col;
parent->_col = bro->_right->_col = Black;
}else if(bro == parent->_right && bro->_left && bro->_left->_col == Red){
RotateRight(bro);
RotateLeft(parent);
bro->_parent->_col = bro->_parent->_left->_col;
parent->_col = bro->_col = Black;
}else if(bro == parent->_left && bro->_left && bro->_left->_col = Red){
RotateRight(parent);
bro->_col = parent->_col;
parent->_col = bro->_col = Black;
}else{
RotateLeft(bro);
RotateRight(parent);
bro->_parent->_col = parent->_col;
parent->_col = bro->_col = Black;
}
}
}
while(_root->_parent) _root = _root->_parent;
return true;
}
总结
红黑树确实是一种比较难学的数据结构,他相对AVL数,哈希表来说他的底层实现可能会有很多的细节,这种时候,一个不小心的失误,你的红黑树可能都会出现比较大的bug,所以练习红黑树的书写也可以帮助我们注意代码中的细节