RBT与AVL树的比较
- AVL:高度要求差不超过1
- 红黑树:RBT要求最长路径不超过短路径的2倍,不需要像AVL一样太平衡,稍微自由,所以旋转较少。
- AVL和RBT树性能比较:
插入同样的数据,AVL树旋转更多,红黑树旋转更少。从查找来说,红黑树略慢,100W红黑树2LogN,需要40次,而AVL100W需要20次,而10亿量级节点时,两者分别是60次和30次。对计算机来说,根本没事。
有人称,AVL树是天才设计,而红黑树是由天才加大佬设计的。
RTB:红黑树
概念:符合二叉搜索树且以属性为红色、黑色的节点组成的树,因此称为红黑树。
红黑树的性质(必须满足以下5点)
- 每个节点不是红就是黑。
- 根节点是黑色的。
- 红色节点的孩子都黑。
- 从任意固定节点到后代所有叶子节点的路径,黑色节点数量相同。
- 叶子都是黑色。(这里的叶子节点指的是空节点NIL)。传统叶子节点如果是黑色,假设两个节点时,有三条路径,如下图,不满足条件4。所以这里的意思是,不是说传统叶子节点,而是空叶子节点必须黑。如下面那张大图。
思考:为什么满足上面条件就能保证:最长路径不超过短路径的2倍?
答:因为性质4(任意节点起到叶子的路径上,黑色节点数量少),假设从根到叶子的所有路径上黑色节点有N个,而最短路径最短为N个黑色节点,最长路径满足黑色N个,所以是红黑红黑…,黑色节点和红色节点数量相同,长度为2N。所以红黑树中,根到叶子最长路径不能超过最短路径两倍。
红黑树实现
红黑树的节点定义:
每个节点需要存KV对,而KV需要使用模板。此外,每个节点和AVL树的节点一样,只是需要多一个红黑属性。每个节点默认给红色属性。此外,枚举类型定义颜色。
template<class K, class V>
struct RBTreeNode
{
//三叉链
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
//存储的键值对
pair<K, V> _kv;
//结点的颜色
int _col; //红/黑
//构造函数
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)
{}
};
//枚举定义结点的颜色
enum Colour
{
RED,
BLACK
};
为什么默认节点颜色为红色?
- 如果插入黑色,那么路径上黑色节点数目就多余其它路径,破坏性质4,需要对红黑树调整。
- 如果插入红色,破坏性质3,需要调整;但是如果父亲是黑色,那就不需要调整。
- 综上,插入红色更方便,总体上调整会少一点。
红黑树的定义
对于一个RBT,主要是给一个根节点。
template <class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv){};
private:
RBTreeNode<K, V>* _root = nullptr;
};
RBT的函数
insert(): 参数:const pair<K, V>& kv, 给红黑树插入的是pair<k, v>,而插入函数返回的类型是:pair<Node*, bool>,意思是告诉你当前这个节点是否插入成功,返回方式是:利用make_pair(_root, true\false);
- 步骤:
- 按BST思路,找合适位置:需要区分是否为根节点。
- 插入树中,且需要父节点
- 插入节点的父亲节点是红色,则需要调整(性质3,不能有连续红)。
- 如果插入节点的父亲是黑,没有破坏性质,不需要调整。
- 调整的三种情况:(性质3较性质4容易维护)以下都是因为出现连续红节点而出现的调整。红黑树得看叔节点情况
情况1. 插入节点的父亲为红,叔叔存在也为红。
因为不能有连续红,把父和叔都变黑,再把祖父变红。相当于,p、u两条路径上多了黑色,而让g变红,和其它路径也保持了黑色节点数量相同。然后继续以祖父为cur(假设它是新插入的红色),继续向上观察它父亲是否也是红色,判断是否需要做因出现连红的现象而调整的操作。只变色即可,且不换位置,不用管p、u相对位置
但是如果祖父节点是根节点,则需要变回黑色。
情况2. 插入节点叔叔存在,叔叔为黑
状态一:祖父、父、儿节点为直线, **当然也可以是向右的一条直线 **。
状态二:祖父、父、儿节点为折线,当然,也可以是g右是p,然后向左是cur。
如上述两种状态的第一张图所示,叔叔为黑,这种情况一定是在红黑树插入不理想而做向上调整过程中出现的。
- 证明如下:
假设给p插入了红色, 造成的该连红局面。设祖父g节点之前,有黑点x个,叔叔之下有黑点y个,则在插入节点的那边cur一路加上g,共有x+1个黑点,而右边一路有x+2(g和u)+y个节点,即使y为0,也说明在插入cur时,各个路上的黑点数量不等,所以情况2一定是向上调整过程中产生的不理想情况。
调整:
状态一时,先以祖父g的右旋,再还是变父p和叔u为黑,祖父g为红;如果是状态一的右直线,先左旋g,再变父p和叔u为黑,祖父为红。
状态二时,以父p节点做左旋,再以祖父g做右单旋,把儿cur变黑,祖父变红;而是向右的折线,先右旋p,再左旋g。
注意:情况2时,调整完就可停止向上。
情况3. 插入节点叔叔不存在
状态一:祖父、父亲、孩子 直线
调整:
先以祖父g右单旋,再变色p为黑。
状态二:祖父、父亲、孩子 折线时
调整:
先以父p左单旋,再以祖父g右单旋,再变cur为黑色,祖父变红。
到这里发现情况2和情况3的折线都需要旋转两次。
- 注意:该情况一定是新插入的节点cur,而不是情况1调整便来的。
因为叔不存在,祖父到null路径上就一个黑色节点,而父p下面一定没有黑色节点了,不然cur之前是祖父,祖父一定有叔叔,且叔父都是黑色。
总结:
上面情况我们发现,需要调整的情况中:
- 情况1:当叔叔节点和父亲节点为红,我们需要同时变黑后使祖父变红,继续向上更新,是根则停止。
- 而情况2、3可以写为一类,因为直线不管是情况2、还是情况3,做法一样,而折线或直线的情况2和情况3,做法一样,我们的情况2、情况3都有直线和折线的两种。uncle在情况2中不涉及变色,所以它不存在也一样的操作。 折线最终都是cur黑,p、g为红。直线最终都是p黑,cur和g红。此外,直线都旋转一次,而折线旋转两次。
- 但又因为折线和直线各自又有向左和向右的区别,所以还是分了4种。所以代码和情况1相对的else是以直线和折线、向左和向右区分的4种情况。
//插入函数
pair<Node*, bool> Insert(const pair<K, V>& kv)
{
if (_root == nullptr) //若红黑树为空树,则插入结点直接作为根结点
{
_root = new Node(kv);
_root->_col = BLACK; //根结点必须是黑色
return make_pair(_root, true); //插入成功
}
//1、按二叉搜索树的插入方法,找到待插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
{
//往该结点的左子树走
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
{
//往该结点的右子树走
parent = cur;
cur = cur->_right;
}
else //已经存在
{
return make_pair(cur, false); //插入失败
}
}
//2、将待插入结点插入到树中
cur = new Node(kv); //根据所给值构造一个结点
Node* newnode = cur; //记录新插入的结点(便于后序返回)
if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
{
//插入到parent的左边
parent->_left = cur;
cur->_parent = parent;
}
else //新结点的key值大于parent的key值
{
//插入到parent的右边
parent->_right = cur;
cur->_parent = parent;
}
//3、颜色调整:当新插节点默认是红,且父也红,连续红色则调整
while (parent && parent->_col == RED) { // 条件一定是父亲存在且父也红
Node* grandfather = parent->_parent;
Node* uncle = nullptr; // 定位uncle,根据父位判断叔
if (parent == grandfather->_left)
uncle = grandfather->_right;
else
uncle = grandfather->_left;
// 情况1:叔存在且红
if (uncle && uncle->_col == RED) {
// 叔叔存在且为红
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else {
// 情况2+3:叔叔不存在或者叔叔存在且为黑
if (parent == grandfather->_left && cur == parent->_left) // 左直线
{
// 此时,左左,右单旋+变色
// 先变色也可以
parent->_col = BLACK;
grandfather->_col = RED;
RotateR(grandfather);
}
else if (parent == grandfather->_right && cur == parent->_right) { // 右直线
// 右右,左单旋
parent->_col = BLACK;
grandfather->_col = RED;
RotateL(grandfather);
}
// 折线情况下,因为情况3的uncle不存在,且情况2uncle颜色不变
// 此外,情况2、3的折线翻转再变色后相同逻辑位置的cur、p、g最终颜色也一样
else if (parent == grandfather->_right && cur == parent->_left) {
// cur为红,parent为红,grandfather为黑。
// 右左双旋。
RotateR(parent);
RotateL(grandfather);
// 记住这里是上黑,下面俩红即可。
cur->_col = BLACK;
grandfather->_col = RED;
}
else if (parent == grandfather->_left && cur == parent->_right) {
RotateL(parent);
RotateR(grandfather);
// 记住这里是上黑,下面俩红即可。
cur->_col = BLACK;
grandfather->_col = RED;
/*RotateL(parent);
std::swap(cur, parent);
parent->_col = BLACK;
grandfather->_col = RED;
RotateR(grandfather);*/
}
break;
}
// 当前是根才做 直接做也行
if (cur == _root) {
cur->_col = BLACK;
}
}
return make_pair(newnode, true);
}
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
//建立subRL与parent之间的联系
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//建立parent与subR之间的联系
subR->_left = parent;
parent->_parent = subR;
//建立subR与parentParent之间的联系
if (parentParent == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
}
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;
//建立subLR与parent之间的联系
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
//建立parent与subL之间的联系
subL->_right = parent;
parent->_parent = subL;
//建立subL与parentParent之间的联系
if (parentParent == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
//左右双旋
void RotateLR(Node* parent)
{
RotateL(parent->_left);
RotateR(parent);
}
//右左双旋
void RotateRL(Node* parent)
{
RotateR(parent->_right);
RotateL(parent);
}
find(): 参数:const K& key (按key值查找)
- 树空,则返nullptr,按左右大小比较的方式走到了空,说明没有这个值。
- 只要当前存在,就按key找。
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first) // 小就给左
cur = cur->_left;
else if (key > cur->_kv.first) // 大就给 右
cur = cur->_right;
else
return cur; // 返回该节点
}
return nullptr;
}
红黑树的验证:
- 首先它是BST树,所以中序是有序的。
- 判断是否是红黑树:通过基本的5条性质来判断。
其中,关于判断性质3不能有连续红节点和性质4黑色节点数不相等的方法。
红黑树判定子函数
- 如果当前到了nullptr,说明到某条空叶路径,且它算黑的,但这里代码中,之前到了空,没算上,所以下面子函数中求黑色节点,也不计算这个。发现数量不对应,就返回错误,否则返回true。
- 当前节点,如果当前节点是红而父节点也红,则返回false。因为不符合性质3
- 当前颜色是黑,则计算一下
- 返回递归左孩子和递归右孩子的情况。
//判断是否为红黑树
//中序遍历
void Inorder()
{
_Inorder(_root);
}
// 判断RBTree:性质1、调用性质3、4
bool ISRBTree()
{
if (_root == nullptr)
return true;
if (_root->_col == RED)
{
cout << "根红error" << endl;
return false;
}
// 以最左路径的黑节点为参考,虽然可能最左是错的,但是我们只关注所有的路径上黑色节点数是否一致
Node* cur = _root;
int blackcount = 0;
while (cur)
{
if (cur->_col == BLACK)
blackcount++;
cur = cur->_left;
}
int count = 0; // 根为黑,以根起始,看每条路径
return _isRBTTree(_root, count, blackcount);
}
private:
//中序遍历子函数
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
// 红黑树的判断
bool _isRBTTree(Node* root, int cur_balck, int total_balck)
{
if (root == nullptr) // 当前走到空,判断这条路径钟点是否黑点一样多
{
if (cur_balck != total_balck)
{
cout << "黑子数量不同" << endl;
return false;
}
return true;
}
// 顺便看 性质3:不能连红:当前红,一定有父亲,因为根不能红,不必判断父存在
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "连续红错误" << endl;
return false;
}
// 当前还没到叶子,如果当前黑,则计数++
if (root->_col == BLACK)
cur_balck++;
return _isRBTTree(root->_left, cur_balck, total_balck) && _isRBTTree(root->_right, cur_balck, total_balck);
}
效果:
红黑树和AVL树的比较
- 行为属性上:
红黑树是较为自由的AVL树,因为它没有严格要求左右高度差,只是控制节点颜色,让最长路径别超最短2倍。 - 效率上:
AVL树复杂在旋转多,而红黑树降低了插入的旋转,在增、删操作中比AVL树更有,使得红黑树应用更广泛。