【C++庖丁解牛】高阶数据结构---红黑树详解(万字超详细全面介绍红黑树)
CSDN 2024-07-22 17:05:34 阅读 66
🍁你好,我是 RO-BERRY
📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识
🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油
目录
前言1.红黑树的概念2.红黑树的性质3.已经学了BST和AVL树,为啥还要学红黑树4.红黑树节点的定义5. 红黑树旋转5.1 左旋示例5.2 右旋示例5.3 代码实现
6.红黑树插入情况一:父节点为空情况二:父节点是黑色情况三:父节点是红色,叔叔也为红色。情况四:父节点是红色,叔叔为黑色。插入小结
7. 删除7.1 删除节点仅存在一个孩子7.2删除节点不存在孩子删除小结
8.红黑树的验证9. 红黑树的查找9. 红黑树与AVL树的比较10.map底层为什么用红黑树实现红黑树:.平衡二叉树(AVL树):
11. 源码11.1 RBTree.h11.2 Test.cpp
前言
这篇文章我们再来学习一种平衡搜索二叉树——红黑树
如果要说常见的数据结构里,哪个数据结构最麻烦、最难以掌握?
绝对非红黑树莫属了,如果只是自己看的话很多人可能看很多遍都不太懂红黑树。
红黑树和AVL树都是常见的自平衡二叉搜索树,它们都可以用于高效地支持插入、删除和查找等操作。虽然它们都能够保持树的平衡性,但在不同的应用场景下,红黑树和AVL树有各自的优势和适用性。
1.红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍(最长路径也不会超出最短路径的两倍,因此红黑树的平衡性要求相对宽松,没有AVL树那样严格),从而使搜索树达到一种相对平衡的状态。
2.红黑树的性质
红黑树是特殊的二叉查找树,又名R-B树(RED-BLACK-TREE),由于红黑树是特殊的二叉查找树,即红黑树具有了二叉查找树的特性,而且红黑树还具有以下特性:
每个节点要么是黑色要么是红色根节点是黑色每个叶子节点是黑色,并且为空节点(还有另外一种说法就是,每个叶子结点都带有两个空的黑色结点(被称为黑哨兵),如果一个结点n的只有一个左孩子,那么n的右孩子是一个黑哨兵;如果结点n只有一个右孩子,那么n的左孩子是一个黑哨兵。)每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
有几点需要注意的是:
特性3中指定红黑树的每个叶子节点都是空节点,但是在Java实现中红黑树将使用null代表空节点,因此遍历红黑树时看不到黑色的叶子节点,反而见到的叶子节点是红色的特性4保证了从根节点到叶子节点的最长路径的长度不会超过任何其他路径的两倍,例如黑色高度为3的红黑树,其最短路径(路径指的是根节点到叶子节点)是2(黑节点-黑节点-黑节点),其最长路径为4(黑节点-红节点-黑节点-红节点-黑节点)。
思考:为什么满足上面的性质,红黑树就能保证:其最长路径中结点个数不会超过最短路径结点个数的两倍?(其实不带第4条就可以,加不加第4条都不会影响每条路径黑色结点数量是否相等)
那通过上面的性质我们可以得知,红黑树中的结点要么是黑色,要么是红色,这没什么可说的;然后要求根结点一定是黑色的,红色结点不能连续出现,如果出现了红色结点,那它的子结点必须是黑色的,然后还要求每条路径黑色结点的数量必须相等。 那这样其实就可以保证一棵红黑树中最长路径不超高最短路径的两倍。 当然实际中不同的红黑树情况是不一样的,所以我们这里来分析一种极端的情况: 大家想,如果一棵红黑树有红有黑,它里面如果有一条全黑的路径,那这条全黑的路径一定就是最短路径; 如果有一条是一黑一红,一黑一红…,这样黑红相间的,那他就是最长的路径。 然后它们里面的黑色结点个数又是相同的的,所以最长路径最多是最短路径的两倍,不可能超过最短路径两倍。 所以这样红黑树的高度就能够保持在一个相对平衡的范围内,当然他就没有AVL树那么严格。 比如这样的
那另外:
其实分析上面的性质,红黑树是可以全黑的,全部黑色结点,只要满足上面的性质即可。
然后大家思考一个问题,上面第四条性质——每个叶子结点都是黑色的(此处的叶子结点指的是空结点,也被称为NIL节点),有什么用?
那大家先算一下这个红黑树有多少条路径?
如果不带空的话,我们可能会认为有5条,但是这里计算路径其实应该走到空(NIL),所以正确的应该是有11条路径。 所有我们可以认为这条规则就是为了更好的帮我们区分不同路径的。
然后再补充一个概念:
结点的黑高(黑色高度):从某结点出发(不含该结点)到达任一空叶结点的路径上经过的黑结点总数
3.已经学了BST和AVL树,为啥还要学红黑树
然后我们再来分析一个问题:
大家想,对于一棵红黑树来说,如果它里面全部的黑色结点一共有N个的话,那它的最短路径长度就差不多是
l
o
g
2
(
N
)
log_2 (N)
log2(N)。 然后整棵树的节点数量就是在【N,2N】之间。 所以最长路径长度就可以认为差不多是2
l
o
g
2
(
N
)
log_2 (N)
log2(N) 所以红黑树的查找最少是
l
o
g
2
(
N
)
log_2 (N)
log2(N)次,最多是2
l
o
g
2
(
N
)
log_2 (N)
log2(N)次,所以红黑树查找的时间复杂度是O(
l
o
g
2
N
log_2 N
log2N),计算时间复杂度前面的常数项可以省略嘛。 而AVL树也是O(
l
o
g
2
N
log_2 N
log2N),但AVL树是比较严格的O(
l
o
g
2
N
log_2 N
log2N),而红黑树是省略了常数项。 所以严格来说,红黑树的查找效率是比不上AVL树的(但对于计算机来说是没什么差别的),但是它们是同一个数量级的,都是O(
l
o
g
2
N
log_2 N
log2N)。
那既然好像都差不多,为什么我们已经学了AVL树了,还要学红黑树呢?红黑树有什么优势吗?
由于AVL树要求更加严格的平衡,所以在进行插入和删除操作时,可能需要更频繁地进行旋转操作来调整树的结构,以保持平衡。相比之下,红黑树的插入和删除操作需要旋转的次数相对较少,因此在插入和删除操作频繁的情况下,红黑树可能更加高效。 这个如果大家学完这篇文章或许能更好的理解。
所以综合而言:
红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。
红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。
相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。
在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。
红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,
但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的. 实际上插入 AVL 树和红黑树的速度取决于你所插入的数据。
如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的。
4.红黑树节点的定义
enum Color{ RED, BLACK};
// 红黑树节点的定义
template<class ValueType>
struct RBTreeNode
{
RBTreeNode(const ValueType& data = ValueType(),Color color = RED)
: _pLeft(nullptr)
,_pRight(nullptr)
, _pParent(nullptr)
, _data(data)
, _color(color)
{ }
RBTreeNode<ValueType>* _pLeft; // 节点的左孩子
RBTreeNode<ValueType>* _pRight; // 节点的右孩子
RBTreeNode<ValueType>* _pParent; // 节点的双亲(红黑树需要旋转,为了实现简单给出该字段)
ValueType _data; // 节点的值域
Color _color; // 节点的颜色
};
思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?
我们来分析一下如果我们插入一个新结点给的是黑色,那它一定会违反上面提到的红黑树的第5条性质——每条路径上黑色结点的数量一致。如图:
因为原来每条路径黑色结点数量是相同的,现在新插入一个黑色结点的话,那插入的这条路径会增加一个黑色结点,但是其它路径数量不变,所以其它所有的路径黑色结点数量都和这一条不相等,这样就比较麻烦了。 那如果我们插入结点默认给红色呢?会违反规则吗? 那红色的话其实要看具体情况了: 如果插入一个红色结点,但是它的父亲也是红色,那就出事了,因为红黑树里面不能出现连续的红色结点,那这种情况就需要调整了。 但如果它的父亲是黑色,那就没问题,不需要进行什么处理。
所以我们新插入的结点给成红色,当然如果是第一次插入根结点我们可以直接给黑色,因为红黑树要求根结点必须是黑色。
5. 红黑树旋转
在分析插入和删除操作前,我们需要先说明一下旋转操作,这个操作在后续操作中都会用得到。旋转操作分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。
5.1 左旋示例
5.2 右旋示例
5.3 代码实现
<code>void RotateL(Node* parent) //左单旋
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
}
void RotateR(Node* parent) //右单旋
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* ppnode = parent->_parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
}
6.红黑树插入
红黑树的插入过程和二叉查找树插入过程基本类似,不同的地方在于,红黑树插入新节点后,需要进行调整,以满足红黑树的性质。性质1规定红黑树节点的颜色要么是红色要么是黑色,那么在插入新节点时,这个节点应该是红色还是黑色呢?
我都想不说答案,大家直接看性质5 (从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。),你插个黑的是不是必然会影响是不是就要调整了,是不是必然就是红的了,对的吧,红的出现两红连续了,可能才需要调整,对的吧,所以插入的节点应该是红色的。
插入我们可能感觉很迷茫,不知道如何下手,这玩意这可咋插入呀,一想到就头大就逃避,哈哈哈,凡复杂事情,其实按情况去划分,把每种情况都克服了,整体也就克服了。我们站在待插入节点的角度,去看我要往哪个父节点下插入,那么看父节点的话,那插入的几种情况我们就来看看哈,我们这里待插入的节点为N节点:
情况一:父节点为空
当父节点为空,也就是到根节点了,因为性质2所以将节点 N 直接变黑作为根节点,即可。
情况二:父节点是黑色
当父节点是黑色,这种情况下,性质4和性质5没有受到影响,不需要调整。
情况三:父节点是红色,叔叔也为红色。
待插入节点N的父节点为红色,叔叔也是红色的情况时,性质4就会被打破,此时需要进行调整。这种情况下,先将 父节点 和 叔叔节点 染成黑色,再将 爷爷节点 的颜色染成红色。此时经过爷爷的路径上的黑色节点数量不变(想想是不是),性质5仍然满足。但需要注意的是 G 被染成红色后,可能会和它的父节点形成连续的红色节点,此时需要递归向上调整。
当然此情况下的这些情况都属于情况三哈。
情况四:父节点是红色,叔叔为黑色。
这种情况跟情况三的不同关键点就在于叔叔的节点颜色,情况三叔叔是红色,而情况四叔叔是黑色的哈。我们的角度从父节点的左右以及待插入节点是父节点的左右可以分成细拆成4种情况,我们来看下这四种情况的处理:
(1)父节点为左子树,待插入节点为父节点的左子树
(2)父节点为左子树,待插入节点为父节点的右子树
小记:当父节点为左子树时,待插入的节点都要往左靠(这个理解不,你看上图待插入N在父节点P的右边时,是不是先左旋了一下,都靠左边了,然后跟待插入节点在左子树时保持都靠左边啦)然后父节点和爷爷换色,然后右旋的。
(3)父节点为右子树,待插入节点为父节点的右子树
(4)父节点为右子树,待插入节点为父节点的左子树
小记:可以看到跟左子树的思路其实差不多,先把节点都往右靠,只是旋转方向不一样,右子树是左旋,左子树是右旋。
插入小结
针对插入我们看到,划分情况来处理哈:
父节点为空的话,就直接变黑即可父节点为黑的话,保持位置不变即可父节点为红的话,就要观察叔叔节点的颜色父节点为红,叔叔为红,直接父亲和叔叔变黑,爷爷变红,然后以爷爷为中心继续递归处理父节点为红,叔叔为黑,就要父亲是左子树还是右子树以及待插入的是父亲的左子树还是右子树,父亲是左子树就要往左靠,所以待插入如果在父亲的右子树就要先左旋,然后父亲和爷爷换色,然后以爷爷为中心右旋哈,父节点为红,叔叔为黑,父亲是右子树就要往右靠,所以待插入如果在父亲的左子树就要先右旋,然后父亲和爷爷换色,然后以爷爷为中心左旋哈。
实现代码:
<code>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为空,这里就是要插入的位置
cur = new Node(kv); // 红色的
//连接
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
//链接父亲
cur->_parent = parent;
//如果插入以后它的parent是红色的,就需要处理
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//如果父亲是爷爷的左孩子,那右孩子就是叔叔
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
// 情况一:叔叔存在且为红
if (uncle && uncle->_col == RED)
{
//变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
//更新cur为grandfather,判断它的父亲是什么情况
//1.如果不存在或者为黑,就需要继续处理,也不会进行循环
//2.如果它的父亲存在且为红,重新循环进行调整
cur = grandfather;
parent = cur->_parent;
}
else
{
// 情况二:叔叔不存在或者存在且为黑
// 旋转+变色
if (cur == parent->_left)
{
// g
// p u
// c
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// p u
// c
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
{
// 情况二:叔叔不存在或者存在且为黑
// 旋转+变色
// g
// u p
// c
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
//g
// u p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
7. 删除
相较于插入操作,红黑树的删除操作则要更为复杂一些。删除操作首先要确定待删除节点有几个孩子,如果有孩子,不能直接删除该节点。我们看上边插入的时候看的是叔叔节点的眼色,而删除是看的待删除节点的兄弟节点的颜色。还是一样我们站在待删除节点的角度,我们可以从如下两个方面去考虑:
节点的颜色,节点有红黑两种可能,那么删除黑节点就会造成节点失衡。节点的构造,有三种可能:其一是删除节点的孩子都为null,其二是删除节点的一个孩子节点为null,其三是删除节点的两个孩子节点都不为null。而如果两个节点都不为null,那么我们就需要找替代节点(前驱或者后继结点)来替代删除,而删除替代结点就会是情况一和情况二。
删除的第三种情况大家应该知道吧,跟我们二叉排序树删除是不是要找一个前驱或者后继节点来替换,然后将前驱或者后继节点的值赋值给待删除的节点,然后删除前驱或者后继节点。而前驱或者后继结点的判断可以使用下图的方式,将节点Key落入X轴中(实质是中序排序)其后一个结点就是后继节点,前一个就是前驱节点,那么我们看一下后继节点替代删除的方案,比如删除-1,那么可以将0替换到-1的位置,然后删除0,这样就回到情况一, 再比如删除4,那么使用5来代替,然后删除5,这样就回到了情况二。
由于前驱和后继至多只有一个孩子节点,这样我们就把原来要删除的节点有两个孩子的问题转化为只有一个孩子节点的问题,问题被简化了一些。这个大家理解的吧,不理解的话建议大家先去看一下二叉排序树的东西,比如二叉排序树的删除,再来这里看红黑树就好理解一点了。
那么针对红黑树的删除,我们这里可分为三种情形:
删除的节点有一个孩子节点,删除节点只能是黑色,其子节点也必为红色,此时用删除节点的子节点接到父节点,且将子节点颜色涂黑,保证黑色数量。
删除的节点没有孩子节点,如果是红色还好直接删除即可,如果是黑色就需要考虑平衡了
删除的节点有两个孩子节点,与二叉排序树一样,使用前驱或者后继节点作为替换的删除节点,场景转至为1或2处理。
接下来我们就仔细分析下情形一和情形二。
7.1 删除节点仅存在一个孩子
删除节点存在一个孩子,那么此时,不需要考虑节点的颜色,因为该节点必然为黑色 (因为性质4,红下必有俩黑或者俩null节点,而我们这里的情况是只有一个孩子,所以必为黑),且孩子节点必然为红(如果为黑,那么D节点就会有一方黑色节点比另一方多一,这个理解么?因为我们说过红黑树的任一黑节点为根的树也必是一颗红黑树)。那么情况一的所有情况如下:
这种情况处理较为简单,只需要将节点P的指针指向DL或者DR,然后将DL或者DR的颜色变更为黑色, 就保证了红黑树的平衡,如下:
上述情况需要注意当D是根节点时,删除操作之后,孩子节点会成为新的根节点
7.2删除节点不存在孩子
删除节点不存在孩子,那么此时就需要考虑节点的颜色。
删除节点为红色
如果为红色,因为性质4那么父节点肯定为黑色,那么直接删除即可哈,P节点断掉指向节点D的指针指向就好了。
删除节点为黑色
我们考虑黑色,这种情况较为复杂,因为黑色节点被删除之后,红黑树会失去平衡,此时需要调整平衡。那么可能的情况有如下几种(如果D为根节点,删除操作后,红黑树变为空树即可,下面以非根节点的情况为例来分析)
父节点为红色,兄弟节点不存在孩子
父节点为红色,兄弟节点不存在孩子(兄弟节点必然存在,且为黑色),这种情况稍微好处理,因为将父节点变为黑色,兄弟节点变为红色,即可保证红黑树平衡。
父节点为红色,兄弟节点存在孩子
父节点为红色,兄弟节点存在孩子,此时无法像情况 3.3.2.1 那么处理,因为兄弟节点存在的孩子必然为红色(理解么?因为我们说了要删除的节点D没有孩子,父亲是红,兄弟就必是黑节点,黑节点下如果还有黑节点,是不是父节点为根的子树就不是红黑树了,对不对),那么此时就需要进行旋转。
父节点为黑色
当兄弟结点不存在孩子节点,此时无法通过旋转来实现平衡,即P节点无法继续调整,那么就B设置为 红色,保证P这个子树平衡,然后通过P节点,向上递归维持平衡。
比如P的父亲是红色的话:
删除小结
(1)删除节点之后,看看这个节点是不是黑色的叶子节点,如果不是,简单处理就可以达到平衡了;
(2)先看N是不是根节点,是的话啥都不用管;不是的话看兄弟什么颜色:
兄弟是红色:进行旋转涂色,去到兄弟为黑色那里处理
兄弟是黑色,看看兄弟子节点是不是全部都是黑。
全黑的话,看父节点什么颜色进行对应处理;
不全黑,看兄在的位置,兄在左的话,看兄的左子是不是红色,进行对应处理;兄在右的话,看兄的右子是不是红色,进行对应处理。
8.红黑树的验证
验证其是否平衡且满足红黑树性质
那如何判断它是否满足是一棵红黑树呢?
其实就是去检查那几条规则(性质):
首先结点颜色要么是黑色,要么是红色,这没什么好检查的。 然后根结点必须是黑色,这个可以检查一下,如果根结点不是黑色(是红色)直接就不符合了
<code>bool IsBalance()
{
if (_root && _root->_col == RED)
return false;
return Check(_root);
}
然后如果出现连续的红色结点,那也不符合。 那怎么检查有没有出现连续红色结点呢? 我们可以去遍历这棵树,然后遇到红色结点判断它的孩子是不是红色结点,如果存在红色结点它的孩子也是红色,那就不符合。 这样确实可以,但是不太好,因为孩子的话要检查两个,而且结点的孩子有还有可能不存在,为空,那还得再加一个判断。 所以我们可以这样做:遍历遇到红色结点我们去看它的父亲,如果它的父亲也为红色那就不行。 而判断父亲的话,只有根结点没有父亲,但是根结点是黑色的也不会去检查它。 所以这样写会好一点
bool Check(Node* cur)
{
if (cur == nullptr)
return true;
if (cur->_col == RED && cur->_parent->_col == RED)
{
cout << cur->_kv.first << "存在连续的红色节点" << endl;
return false;
}
return Check(cur->_left)
&& Check(cur->_right);
}
bool IsBalance()
{
if (_root && _root->_col == RED)
return false;
return Check(_root);
}
然后还剩一个,我们要检查每条路径黑色结点数量是否相等,存在不相等的情况就不符合。 那这个要如何检查呢? ,我们可以先求出一条路径的黑色结点数量,把它作为基准值,然后再递归求出每条路径的黑色结点数量和它比较,如果存在不相等的情况,就不符合。
bool Check(Node* cur, int blackNum, int refBlackNum)
{
if (cur == nullptr)
{
//走到空就是一条路径走完了,比较一下是否相等
if (refBlackNum != blackNum)
{
cout << "黑色节点的数量不相等" << endl;
return false;
}
//cout << blackNum << endl;
return true;
}
if (cur->_col == RED && cur->_parent->_col == RED)
{
cout << cur->_kv.first << "存在连续的红色节点" << endl;
return false;
}
if (cur->_col == BLACK)
++blackNum;
return Check(cur->_left, blackNum, refBlackNum)
&& Check(cur->_right, blackNum, refBlackNum);
}
bool IsBalance()
{
if (_root && _root->_col == RED)
return false;
//先求出一条路径黑色结点数量
int refBlackNum = 0;
Node* cur = _root;
while (cur)
{
if(cur->_col == BLACK)
refBlackNum++;
cur = cur->_left;
}
//检查是否出现连续红色结点及所有路径黑色结点数量是否相等
return Check(_root, 0, refBlackNum);
}
9. 红黑树的查找
那红黑树的查找就也和搜索二叉树的查找一样,之前讲过,这里就不再说了。
9. 红黑树与AVL树的比较
红黑树和AVL树都是高效的自平衡搜索二叉树,增删改查的时间复杂度都是O(
l
o
g
2
N
log_2 N
log2N)。 红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,所以AVL树的插入和删除操作比红黑树更耗时。 因为AVL树在插入和删除节点后,会进行更多的旋转操作以维持一个较为严格的平衡,所以插入和删除操作的时间复杂度更高。 相对而言,红黑树对平衡的控制比较宽松,降低了插入删除时需要旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。 在实际应用中,红黑树的使用更广泛。许多编程语言和库都使用红黑树作为基础数据结构,例如C++ STL中的std::map和std::set就是基于 红黑树实现的。
10.map底层为什么用红黑树实现
红黑树:
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。
通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,
因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。
性质:
每个节点非红即黑根节点是黑的;每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;如果一个节点是红色的,则它的子节点必须是黑色的。对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;
.平衡二叉树(AVL树):
红黑树是在AVL树的基础上提出来的。
平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。
AVL树中所有结点为根的树的左右子树高度之差的绝对值不超过1。
将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
11. 源码
11.1 RBTree.h
#pragma once
enum Colour
{
RED,
BLACK,
};
template <class T>
struct RBTreeNode
{
RBTreeNode<T>* _parent;
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
T _data;
Colour _col;
RBTreeNode(const T& data)
:_parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, _data(data)
, _col(RED)
{ }
};
template <class T>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
bool Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (data < cur->_data)
{
parent = cur;
cur = cur->_left;
}
else if (data > cur->_data)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//走到这里cur为空,就是key应该插入的位置
cur = new Node(data);
//链接
if (data < parent->_data)
parent->_left = cur;
if (data > parent->_data)
parent->_right = cur;
//链接父亲指针
cur->_parent = parent;
//如果插入之后它的parent是红的,就需要进行调整
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//如果父亲是祖父的左孩子,那右孩子就是叔叔
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//这里处理的情况是叔叔存在且为红,变色+向上继续处理
if (uncle && uncle->_col == RED)
{
//将p,u改为黑,g改为红
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
//更新cur为grandfather,判断它的父亲是什么情况:
//1.如果不存在或者为黑,就需要继续处理了,也不会进行循环了
//2.如果它的父亲存在且为红,重新循环进行调整
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在/叔叔存在且为黑的情况
{
// g
// p u
// c
if (cur == parent->_left)//左左——右单旋+变色
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//左右——左右双旋+变色
{
// g
// p u
// c
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
//这里调整完就可以break了,因为颜色都变的合适了,而相关的链接关系旋转会帮我们处理
break;
}
}
//如果父亲是祖父的右孩子,那左孩子就是叔叔
else //parent = grandfather->_right
{
Node* uncle = grandfather->_left;
//这里处理的情况是叔叔存在且为红
if (uncle && uncle->_col == RED)
{
//将p,u改为黑,g改为红
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
//更新cur为grandfather,判断它的父亲是什么情况:
//1.如果不存在或者为黑,就需要继续处理了,也不会进行循环了
//2.如果它的父亲存在且为红,重新循环进行调整
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)//右右——左单旋+变色
{
// g
// u p
// c
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//右左——右左双旋+变色
{
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
//上面处理过程中有可能会把根变成红色,这里统一处理一下,把根置成黑
_root->_col = BLACK;
return true;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
bool IsBlance()
{
if (_root && _root->_col == RED)
{
cout << "根结点是红色" << endl;
return false;
}
//先求出一条路径黑色结点数量
int mark = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
++mark;
cur = cur->_left;
}
//检查是否出现连续红色结点及所有路径黑色结点数量是否相等
return _Check(_root, 0, mark);
}
int TreeHeight()
{
return _TreeHeight(_root);
}
private:
int _TreeHeight(Node* root)
{
if (root == nullptr)
return 0;
int RightH = _TreeHeight(root->_left);
int leftH = _TreeHeight(root->_right);
return RightH > leftH ? RightH + 1 : leftH + 1;
}
bool _Check(Node* root, int blackNum, int mark)
{
if (root == nullptr)
{
//走到空就是一条路径走完了,比较一下是否相等
if (blackNum != mark)
{
cout << "存在黑色结点数量不相等" << endl;
return false;
}
return true;
}
if (root->_col == BLACK)
++blackNum;
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "出现连续红色结点" << endl;
return false;
}
return _Check(root->_left, blackNum, mark)
&& _Check(root->_left, blackNum, mark);
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_data << " ";
_InOrder(root->_right);
}
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//旋转并更新_parent指针
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//先保存一下parent->_parent,因为下面会改它
Node* pparent = parent->_parent;
//旋转并更新_parent指针
subR->_left = parent;
parent->_parent = subR;
//若pparent为空则证明旋转的是一整棵树,因为根结点的_parent为空
if (pparent == nullptr)
{
//subR是新的根
_root = subR;
_root->_parent = nullptr;
}
//若pparent不为空,则证明旋转的是子树,parent上面还有结点
else
{
//让pparent指向子树旋转之后新的根
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else
{
pparent->_right = subR;
}
//同时也让新的根指向pparent
subR->_parent = pparent;
}
}
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//旋转并更新_parent指针
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
//先保存一下parent->_parent,因为下面会改它
Node* pparent = parent->_parent;
//旋转并更新_parent指针
subL->_right = parent;
parent->_parent = subL;
//若parent等于_root则证明旋转的是一整棵树(这也是一种判断方法)
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
//让pparent指向子树旋转之后新的根
if (parent == pparent->_left)
{
pparent->_left = subL;
}
else
{
pparent->_right = subL;
}
//同时也让新的根指向pparent
subL->_parent = pparent;
}
}
private:
Node* _root = nullptr;
};
11.2 Test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <time.h>
#include "RBTree.h"
void RBTest1()
{
//int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
//int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int arr[] = { 95,47,32,29,7,7,2,50,74,30 };
RBTree<int> t1;
for (auto e : arr)
{
t1.Insert(e);
}
t1.InOrder();
cout << t1.IsBlance() << endl;
}
void RBTest2()
{
srand((unsigned int)time(nullptr));
const int N = 100000;
RBTree<int> t;
for (int i = 0; i < N; ++i)
{
int x = rand() + i;
t.Insert(x);
}
cout << t.TreeHeight() << endl;
AVLTree<int, int> t2;
for (int i = 0; i < N; ++i)
{
int x = rand() + i;
t2.Insert(make_pair(x, x));
}
cout << t2.TreeHeight() << endl;
}
int main()
{
RBTest2();
return 0;
}
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。