一、红黑树简介
1.1红黑树的性质
不同于AVL树的严格平衡,红黑树更多的是一种近似平衡,它符合如下性质:
①每个节点不是红色就是黑色
②根节点是黑色的
③如果一个节点是红色,那它的两个孩子是黑色的
这一条限制了一条路径没有连续的红色节点
④对于每个节点,对于该节点到其后代所有叶节点的简单路径上,均包含相同数目的黑色节点
这一条限制了每条路径的黑色节点数目相同
⑤(只用于判断路径条数)每个叶子节点都是黑色的,这里的叶子节点指的是空节点,而不是常规意义上的完整节点
这里的叶子节点又称NIL节点,在判断路径条数时候使用,每一个NIL节点都对应一条路径
综上,通过保持以上性质,红黑树可以保证:最长路径的节点个数不超过最短路径的二倍
即 最短路径长度*2 >= 最长路径长度
1.1补:空树也是红黑树
1.2一种极端情况下的红黑树
对于一颗红黑树而言,在规则限制下的最短路径为全黑的路径,而最长路径为一黑一红的路径,例如:
它拥有规则限制的两种极端情况:
其最短路径就是13->8
最长路径就是13->17->25->27
但是,实际上在一棵红黑树当中,规则限制的两种极端情况可能并不存在,此时最短路径和最长路径以实际为准
1.3红黑树与AVL树效率的比较
①在查找的时候,严格来说AVL树的效率为logn,而红黑树中最长路径的效率为2*logn,此时的AVL树效果要好,但实际在查找操作执行的时候,logn与2*logn并不会在时间上差别明显
②在插入和删除的时候,红黑树往往更具有优势
1.4红黑树节点的结构
红黑树的结构不再需要平衡因子,但是需要一个新的类型“颜色”,我们可以通过枚举来实现这一新的类型
enum Color
{
RED,
BLACK
};
此外还需要二叉搜索树中基本的几个属性构成一个三叉链表,因为要控制颜色,所以依旧需要存储父节点:
template<class K, class V>
struct RBTreeNode
{
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Color _col;
};
二、红黑树的模拟实现——插入
2.1插入时的颜色设置
我们在插入一个新节点时,不论插入红色还是黑色,都会不可避免地对红黑树的性质造成改变,但假设两种不同情况并观察会发现:
①若插入黑色节点,一定会破坏规则4
②若插入红色节点,可能破坏规则3,但不一定会破坏;此外还需要注意插入根节点需要变色一次
综上,对比两种并选择一种最优的方式,我们规定新插入节点是红色节点
2.2插入时的情况分析
为了便于介绍,我们标记几个符号:
g:grandfather
p:parent
u:uncle
cur:插入的新节点
2.2.1情况一:cur为红,p为红,g为黑,u存在且为红
抽象图:
abcde为抽象内容,我们的处理方式是:p和u改成黑色,g改为红色,然后把g当成cur继续向上调整,如果g是根节点,则还需要把颜色变回黑色
原图: 处理后:
①当abcde都为空的时候
此时情况较为简单,只需要按照标准变换方式对颜色进行变换即可
补:为什么要把g变红?
我们抽象图对应的可能是整棵树,也可能是一棵树的子树,
如果是一棵树的子树,g仍然为黑的话会导致该子树的黑色节点数目多1个,会违反规则4(这样其实和插入一个黑色节点造成的影响类似了)
②当cde是具有一个黑色节点的红黑树
我们应该知道,此时的cur起初不是红色,而是经过下面节点进行一次变换后,cur代替了原来的grandfather而变成红
即起初:
在经过了一次变换后才形成之前的情况,此时新节点插入的位置可能是ab四个孩子的位置
补:分析一下此时的可能数目
此时的cde均为有一个黑节点的红黑树,均有三种可能的情况,加上cur插入的四种可能
综上共有:3*3*3*4=108种可能情况
③当cde是具有两个黑色节点的红黑树
此时的cur是经过两次变换才变红的,而对于一个拥有两个黑色节点的红黑树而言,可能的情况数目会出现爆炸式增长,而更多的黑色节点难以直接计算
2.2.2情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑
抽象图:
abcde为抽象内容,我们的处理方式主要是旋转,因为此时已经难以通过单纯的变换颜色来控制平衡了,这里我们暂时只抽象
p为g的左孩子
cur为p的左孩子
这种情况
原图: 处理后:
①当u不存在时,abcde一定为空
1> p为g的左孩子,cur为p的左孩子
处理方式:进行以g为parent的右单旋,旋转后p变黑,g变红
原图: 处理后:
2>p为g的左孩子,cur为p的右孩子
处理方式:进行左右双旋,旋转后cur变黑,g变红
原图: 处理后:
3>p为g的右孩子,cur为p的右孩子
处理方式:进行以g为parent的左单旋,旋转后p变黑,g变红
原图: 处理后:
4>p为g的右孩子,cur为p的左孩子
处理方式:进行右左双旋,旋转后cur变黑,g变红
原图: 处理后:
②当u存在且为黑的时候
1> de为红或空,c为有一个黑色节点的红黑树
cur原来是黑,由情况一变过来
处理:先进行情况一的变色,然后以parent进行右单旋,旋转后p变黑,g变红
先变色: 再右单旋:
补:分析一下此时的可能数目
de:可能性都有两种,红和空
新节点:有四个可能插入的位置
c:可能性有三种
综上,2*2*3*4=48种不同可能
⭐情况汇总:
观察发现,在u存在且为黑的时候进行旋转与变色的处理过程都与u不存在的时候完全一样,我们由此可以推出他们其实属于同种处理情况。(余下处理方式参考u不存在即可)
2> de为有一个黑色节点的红黑树,c为有两个黑色节点的红黑树
cur经过两次情况一变换过来,同样会发生情况数的爆炸式增长
2.3插入的逻辑与代码实现
首先按照搜索二叉树的插入逻辑检查插入值的key是否重复,完成后进入通过颜色控制树的平衡这一过程:
大循环利用parent是否存在来控制:
2.3.1parent不存在:
插入的节点cur就是根节点,此时只需要把他的颜色变黑即可
2.3.2parent存在且为黑:
因为插入之前一定是一个标准的红黑树,所以此时parent为黑就说明循环更新已完成
2.3.3parent存在且为红:
需要继续循环处理,且因为parent为红节点而整棵树根节点应该是黑色的,所以grandfather一定存在,此时我们只需要关注uncle的情况即可
2.3.3.1uncle存在且为红:对应情况一
此时只要把parent和uncle变黑,把grandfather变红,
将grandfather置为新的cur,parent置为garandfather的parent,
再次进入循环即可
2.3.3.2uncle不存在:对应情况二第一种
此时可以确定cur就是新插入的节点,因为如果cur为更新上来的话黑色节点数目就不对了;
此时我们的处理可以按照2.2.2-①中的四种情况分别进行
2.3.3.3uncle存在且为黑:对应情况二第二种
此时可以确定cur一定经过情况一的变换,因为uncle为黑的时候,cur只有本来是黑才不会违反规则,此时cur一定会经历一个变色的过程,也就是情况一对应
此时我们的处理也可以按照2.2.2-①中的四种情况分别进行,因为在2.2.2-②中我们对情况进行总结时发现的结论在这个时候正好可以起作用
⭐2.3.3补:情况二会直接结束循环
除了情况一对应变色会导致grandfather变红,其他两种都对应情况二,在旋转之后grandfather都会变黑,即大循环已经无需再进行
2.3.4代码实现参考
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);
//没有重复,parent此时指向要插入节点的父节点,cur直接存新节点
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//如果parent是grandfather的左孩子
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// g
// p
//c
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
// g
// p
// c
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
}
}
else
{
// g
// p
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
// g
// p
// c
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
// g
// p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
}
}
}
_root->_col = BLACK;
return true;
}
2.4判红黑树是否平衡
2.4.1.逻辑分析
判断的过程还是要回归到红黑树的性质上
第一点不需要刻意实现,第二点用一个判断即可,关键在第三点和第四点上
第三点我们可以通过判断每个红色节点的父节点是否是红色来完成
而第四点我们的实现思路是:要计算每条路径上黑色节点的数目,可以在每个节点位置记录一个值num,这个值代表根节点到当前节点上的黑色节点总数,
要完成整棵树的遍历并且比较所有节点,我们选择使用前序递归遍历的方式,在递归前获取一个num参考值,以便在递归过程中与所有走完路径节点对应的num进行比较
2.4.2代码参考
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);
}
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);
}