【C++庖丁解牛】高阶数据结构---红黑树详解(万字超详细全面介绍红黑树)

目录
- 前言
- 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.h
- 11.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 代码实现
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)父节点为右子树,待插入节点为父节点的左子树

小记:可以看到跟左子树的思路其实差不多,先把节点都往右靠,只是旋转方向不一样,右子树是左旋,左子树是右旋。
插入小结
针对插入我们看到,划分情况来处理哈:
- 父节点为空的话,就直接变黑即可
- 父节点为黑的话,保持位置不变即可
- 父节点为红的话,就要观察叔叔节点的颜色
- 父节点为红,叔叔为红,直接父亲和叔叔变黑,爷爷变红,然后以爷爷为中心继续递归处理
- 父节点为红,叔叔为黑,就要父亲是左子树还是右子树以及待插入的是父亲的左子树还是右子树,父亲是左子树就要往左靠,所以待插入如果在父亲的右子树就要先左旋,然后父亲和爷爷换色,然后以爷爷为中心右旋哈,
- 父节点为红,叔叔为黑,父亲是右子树就要往右靠,所以待插入如果在父亲的左子树就要先右旋,然后父亲和爷爷换色,然后以爷爷为中心左旋哈。
实现代码:
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// cRotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// g// p u// cRotateL(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// cif (cur == parent->_right){RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// g// u p// cRotateR(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.红黑树的验证
验证其是否平衡且满足红黑树性质
那如何判断它是否满足是一棵红黑树呢?
其实就是去检查那几条规则(性质):
首先结点颜色要么是黑色,要么是红色,这没什么好检查的。 然后根结点必须是黑色,这个可以检查一下,如果根结点不是黑色(是红色)直接就不符合了
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// cRotateL(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// cRotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else//右左——右左双旋+变色{// g// u p// cRotateR(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;}//同时也让新的根指向pparentsubR->_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;}//同时也让新的根指向pparentsubL->_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;
}
相关文章:
【C++庖丁解牛】高阶数据结构---红黑树详解(万字超详细全面介绍红黑树)
🍁你好,我是 RO-BERRY 📗 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 目录 前言1.红黑树的概念2.红黑…...
汽车网络安全管理
汽车网络安全管理 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,…...
文本自动粘贴编辑器:支持自动粘贴并筛选手机号码,让信息处理更轻松
在信息时代的浪潮中,文本处理已成为我们日常工作与生活的重要组成部分。无论是商务沟通、社交互动还是个人事务处理,手机号码的筛选与粘贴都显得尤为关键。然而,传统的文本处理方式效率低下、易出错,已无法满足现代人的高效需求。…...
Linux云计算之网络基础9——园区网络架构项目
要求构建大型园区网络架构,方案如下: 园区A 园区c 公司B 要求: 1、A公司园区网络 一台汇聚层三层交换机,两台接入层二层交换机。 出口有一台路由器。 2、A园区有五台服务器。 分别为两台 WEB 服务器,…...
Java 中的 List 集合
文章目录 添加元素获取元素检查元素删除元素修改元素获取列表大小检查列表是否为空清空列表查找元素索引获取列表的子列表 List 是 Java 集合框架中的一个接口,它表示一个有序的集合(序列),允许存储重复的元素。List 接口提供了许…...
数据库之DDL操作(数据库,表,字段)
Data Definition Language,数据库定义语言,用来定义数据库对象(数据库,表,字段) 1.数据库操作 1.1查询所有数据库 show databases; 1.2查询当前数据库 show databases(); 1.3创建数据库 create da…...
5.3.1 配置交换机 SSH 管理和端口安全
5.3.1 实验1:配置交换机基本安全和 SSH管理 1、实验目的 通过本实验可以掌握: 交换机基本安全配置。SSH 的工作原理和 SSH服务端和客户端的配置。 2、实验拓扑 交换机基本安全和 SSH管理实验拓扑如图所示。 交换机基本安全和 SSH管理实验拓扑 3、实验步骤 &a…...
Django--数据库连接
数据库配置 打开mysite/settings.py配置文件,这是整个Django项目的设置中心。Django默认使用SQLite3数据库,因为Python原生支持SQLite3数据库,所以你无须安装任何程序,就可以直接使用它。 下面是默认的数据库配置: …...
CKA 基础操作教程(二)
Kubernetes Deployment 理论学习 Kubernetes Deployment (部署)是一种 Kubernetes 资源对象,用于定义和管理容器化应用程序的部署和更新。Deployment 提供了一种声明性的方式来定义应用程序的期望状态,并负责确保所需数量的 Pod…...
【SQLServer】快速查看SQL Server中所有数据库中所有表的行数
1.查看某个数据库中每个表的行数 SELECT @@servername as servername, db_name() as databasename, s.name AS schemaname, t.name AS tablename,p.rows AS rowcounts,SUM(a...
Node.js------Express
◆ 能够使用 express.static( ) 快速托管静态资源◆ 能够使用 express 路由精简项目结构◆ 能够使用常见的 express 中间件◆ 能够使用 express 创建API接口◆ 能够在 express 中启用cors跨域资源共享 一.初识Express 1.Express 简介 官方给出的概念:Express 是基…...
CSS - 你实现过0.5px的线吗
难度级别:中级及以上 提问概率:75% 我们知道在网页显示或是网页打印中,像素已经是最小单位了,但在很多时候,即便是最小的1像素,精度却不足以呈现所需的线条精度和细节。因此,为了在网页显示和网页打印中呈现更加细致的线条,为了在视觉…...
hbuilderX创建的uniapp项目转移到vscode
场景:一直使用hbuilderX开发的朋友想转移到vscode获取更好的TypeScript支持,所以想把整个项目目录拖到vscode进行开发,但发现运行不了,提示没有package.json等,并且不能执行pnpm命令 首先,我们先来看一下h…...
JavaScript 事件流
JavaScript与HTML之间的交互是通过事件实现的,而用户与浏览器页面的互动也是通过事件来实现的事件就是文档或浏览器窗口中发生的一些特定的交互瞬间,所以分为两种事件,一是发生在 浏览器对象(BOM)上的事件,…...
HTML——5.表单、框架、颜色
一、表单 HTML 表单用于在网页中收集用户输入的数据,例如登录信息、搜索查询等。HTML 提供了一系列的表单元素,允许用户输入文本、选择选项、提交数据等。 <!DOCTYPE html> <html lang"en"> <head> <meta charset&q…...
Docker、Kubernetes之间的区别
比较容器化工具:了解 Docker、Kubernetes 在应用程序部署和管理方面的差异。 基本概述 Docker 是一个流行的容器化平台,允许开发人员在容器中创建、部署和运行应用程序。 Docker 提供了一组工具和 API,使开发人员能够构建和管理容器化应用程…...
【21-40】计算机网络基础知识(非常详细)从零基础入门到精通,看完这一篇就够了
【21-40】计算机网络基础知识(非常详细)从零基础入门到精通,看完这一篇就够了 以下是本文参考的资料 欢迎大家查收原版 本版本仅作个人笔记使用21、HTTPS是如何保证数据传输的安全,整体的流程是什么?(SSL是…...
软考111-上午题-【计算机网络】-URL和DNS
一、URL解析 org:各类组织结构(非盈利团队) 1-1、顶级域 顶级域名是域名的最后一个部分,即是域名最后一点之后的字母,例如:www.baidu.com这个域名中,顶级域是.com(或.COMÿ…...
EasyCVR视频汇聚平台海康Ehome2.0与5.0设备接入时的配置区别
安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…...
echarts实现炫酷科技感的流光效果
前言: echarts实现炫酷科技感的流光效果 效果图: 实现步骤: 1、引入echarts,直接安装或者cdn引入 npm i echarts https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js 2、封装 option方法,第一个数据是折线数据&a…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...
Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...
简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
