注:本文为 “红黑树” 相关文章合辑。
未整理去重,如有内容异常请看原文。
Red-Black Tree / 红黑树
AWN
Feb 7, 2020

from Beenabug44 on pixabay.com .
树的搜寻 (Tree Search),一直是电脑科学领域的重要算法,当中探讨了树可能遇到的问题:树成长时可能偏重于特定一边,即不平衡 (Unbalance) 的现象。二元树是常见且广泛使用的一种树,面临这样关乎运气、可能退化成连结串列 (Linked List) 的潜藏缺点,在使用上免不了让人担心效能是否能常保顺畅。此外,在一些应用上,可能不期望这样不平衡的可能性发生,所以具有自动平衡左右数量分布效果的算法早在约 1962 年便被发表出来,称为 AVL 树 (Adelson-Velsky and Landis Tree, AVL Tree)。这种平衡成长的二元搜寻树 (Binary Search Tree, BST) 被归类称为自平衡二元搜寻树 (Self-Balancing Binary Search Tree)。
接下来,要介绍同归为自平衡二元搜寻树的红黑树 (Red-Black Tree, RBT or RB Tree) 对平衡性的要求比 AVL 树还宽松。红黑树是利用节点颜色来检视二元树每条延展的路径高度是否差不多,因此发明者订立了以下几点规则:
- 树上的每个节点 (node) 只能是红色或黑色
- 根节点 (root) 一定要是黑色
- 叶节点 (leaf) 一定要是黑色的空值 (NULL)
- 红色节点的两个子节点 (child) 一定要是黑色 (亦即不能有两个红色节点相连,注意:黑色节点的子节点颜色没有限制)
- 从任何节点出发,其下至叶节点所有路径的黑色节点数目要相同
满足上述规则的二元树,相比一般的二元树更能保持平衡性,往后套用二元树的算法来查找资料时能更快速、方便地到达目的地,套用运算的时间复杂度为 O(log n),这是因为红黑树保证最长路径不会超过最短路径的两倍。
因为红黑树也是二元树的一种,所以例如:新增节点、删除节点、查询节点等针对红黑树的操作与二元树的操作前段算法相同,只是在每次操作完毕之后可能会让树的结构改变而可能无法满足成为红黑树的性质,进而可能不具有平衡的性质。为了在操作后仍是一棵红黑树 (满足上述五项规则),以下有两项基本运算可以用来帮助调整树状结构以满足红黑树的规则,这两个运算分别为变色与旋转 (左旋、右旋)。
复习小教室:Operations on Binary Search Tree
- 常见的操作包含:SEARCH, MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR,其中搜寻节点(Search)、找最大节点(Maximum)、找最小节点(Minimum)较单纯,不多介绍。这边想多着墨于找寻上位者(Successor)与下位者(Predecessor)的操作上,这与之后新增(Insertion)与删除(Deletion)节点的操作具有高度关联:
Successor(z):寻找数列的下一个数字 (下位者)。如果右子树非空,则找右子树中最小的节点;如果右子树为空,则往回找第一个比自己大的祖先。
Predecessor(z):寻找数列的上一个数字 (上位者)。如果右子树非空,则找右子树中最大的节点;如果右子树为空,则往回找第一个比自己小的祖先。
Deletion(z):删除特定节点,由子节点的三种情况来讨论算法。没有子节点或只有一个子节点的时候,直接将该节点删除,其父节点则直接连接至其子节点(或为NULL)上。但有两个子节点时,则需要先找到欲删除节点的下位者是谁,将下位者与欲删除节点的数值交换,再将下位者(已换成欲删除节点数值)删除,其中会将下位者的父节点与下位者的子节点相连
(有两个子节点时,一般是以右边最小的子节点、或者左边最大的子节点来递补)。
首先是变色,这个运算很容易理解,就是将当前颜色改变成另一个颜色,例如红色改成黑色。但是很多时候用到的运算是旋转:

Left Rotation on Node P

Right Rotation on Node P
如上是以节点 P 为中心进行旋转运算。基本上维持红黑树的演算过程都是由变色与旋转依次组成。总结来说,红黑树的新增与删除操作是先透过一般二元树的新增与删除操作后,再从递补的节点开始向上进行红黑树性质维护。接下来,直接用例子演示走过一遍新增与删除节点的算法更能了解到变色与旋转的作用为何:
新增 (Insertion)
在新增操作上,新插入的节点一律都为红色,目的是希望红黑树维持规则 5 的约束,但也可能会违反除了规则 5 以外的其他规则,所以作完二元树新增节点的操作后,需要以新增的节点开始向上检查红黑树是否符合各项规则,修正红黑树的算法可能会有以下几种情境,因应不同情境会采取不同的修正过程:
情境一:红黑树为空,插入红节点后成为根结点,会违反规则 2,需将该节点改为黑色,即可完成红黑树维护。

Case 1: Red Node on the Empty RB Tree
情境二:在黑节点上与红节点 z 相接,不违反任何规则,无须作其他调整。

Case 2: Red Node on a Black Node
情境三:在红节点上与红节点 z 相接,会违反规则 4。从自己开始维护,若叔叔节点为红色,则先将父节点涂黑,用以保持规则 4,但这时会违反规则 5,所以须再将祖节点与叔节点分别涂成红色和黑色。接着再从祖节点继续往上检查维护。
如图片,可以看到最后的结果违反规则 4,该情境符合情境四所示,其父节点与自身皆为红色,所以下一轮会再以对应的流程处理。

Case 3: Red Node on a Red Node with its Red Uncle Node
情境四:在红节点上与红节点 z 相接,会违反规则 4。从自己开始维护,若叔叔节点为黑色且自己位置在父节点右边的状况,则先以父节点进行左旋,形成情境五,再作为情况五进行下一轮的处理。

Case 4: Red Node on the Right Side of Red Node with its Black Uncle Node
情境五:在红节点上与红节点 z 相接,会违反规则 4。从自己开始维护,若叔叔节点为黑色且自己位置在父节点左边的状况,则先将父节点改为黑色,用以保持规则 4,但这时会违反规则 5,所以须再将祖节点涂成红色,接着再以祖节点进行右旋,即能完成维护。(直觉上,节点 F 的左边路径会多一个黑色,可以透过右旋把黑色转掉)

Case 5: Red Node on the Left Side of Red Node with its Black Uncle Node
删除 (Deletion)
在删除操作上,删除一个节点时可能也会违反规则,所以作完二元树删除节点的操作后,视需要以递补节点开始向上检查红黑树是否符合各项规则,修正红黑树的算法可能会有以下几种情境,因应不同情境会采取不同的修正过程:
**情境一:**删除红节点,不违反任何规则,无须进行维护。

Case 1: Deletion on Red Node
情境二:删除节点为黑色需要进行维护。删除黑节点后,如果递补上来的节点为红色时,因为黑节点被删除会违反规则 5,所以直接将递补的红节点涂黑即可。或者,如果维护点在根结点时,如果根节点为红色,这时则违反规则 2,直接将根节点涂黑即可。

Case 2: Deletion on Black Node with Red Node Compensation
情境三:删除节点为黑色需要进行维护。删除黑节点后,如果递补上来的节点为黑色时,需要视情况补足路径上缺失的一个黑色,会以递补节点作为当前节点开始进行维护:
兄弟节点若是红色,则将兄弟节点涂黑、父节点涂红、再以父节点为基准左旋 (因为节点 C 的左边路径会少一个黑色,直觉会是透过左旋把黑色补起来)。但是可以发现现在的红黑树还是违反规则 5,虽然无法完全解决问题,但是递补节点的兄弟节点改变了,可以新情境再下一轮进行对应的修正。

Case 3: Deletion on Black Node with Black Node Compensation and Red Sibling Node
情境四:删除节点为黑色需要进行维护。删除黑节点后,如果递补上来的节点为黑色时,需要视情况补足路径上缺失的一个黑色,会以递补节点作为当前节点开始进行维护:
兄节点若是黑色,且兄节点之左节点与右节点同为黑色的时候,则将兄节点涂红。这时若父节点为红色则结束修正后将父节点涂黑即可,但是如果父节点为黑色,则需要再从父节点开始进行修正 (因为也要照顾到父节点的兄弟子树,看看是否也满足规则 5)。

Case 4: Deletion on Black Node with Black Node Compensation and Black Sibling Node with its Two Black Child Nodes
情境五:删除节点为黑色需要进行维护。删除黑节点后,如果递补上来的节点为黑色时,需要视情况补足路径上缺失的一个黑色,会以递补节点作为当前节点开始进行维护:
兄节点若是黑色,且兄节点之左节点为红色、右节点为黑色的时候,则将兄节点涂红、兄节点之左节点涂黑,再以兄节点为基准右旋,接着再进行下次的修正 (若采用左旋会变成相同情境,所以只好右旋,才可能跳离循环)。

Case 5: Deletion on Black Node with Black Node Compensation and Black Sibling Node with its Red Left Child Node
情境六:删除节点为黑色需要进行维护。删除黑节点后,如果递补上来的节点为黑色时,需要视情况补足路径上缺失的一个黑色,会以递补节点作为当前节点开始进行维护:
兄节点若是黑色,且兄节点之右节点为红色的时候,则将兄节点涂成与父节点相同的颜色、父节点涂黑、兄节点的右节点涂黑,再以父节点为基准左旋即可。(透过左旋与涂色操作将右边的黑色往左调整、并将右边红点涂黑补足缺失,希望符合规则 5 来让每条路径上黑色节点的数量一致)

Case 6: Deletion on Black Node with Black Node Compensation and Black Sibling Node with its Red Right Child Node
红黑树谨遵单单五条规则,前人总结了各种情况后,列出对应的处理方式。事后理解情境的对应处理方式虽然有迹可循,但是窥探其中奥秘却也令人慑服,很难想像这项算法从无到有所付出的庞大心力与挫折!
Reference
- Wikipedia Contributors, “Red–black tree,” January 23, 2020. [Online]. Available: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree. [Accessed February 07, 2020].
- ITREAD01, “资料结构与算法:红黑树 (Red Black Tree),” January 06, 2019. [Online]. Available: https://www.itread01.com/content/1546725999.html. [Accessed February 07, 2019].
- Chiu CC, “Red Black Tree: Insert(新增资料)与Fixup(修正),” January 27, 2016. [Online]. Available: http://alrightchiu.github.io/SecondRound/red-black-tree-insertxin-zeng-zi-liao-yu-fixupxiu-zheng.html. [Accessed January 30, 2020].
- Chiu CC, “Red Black Tree: Delete(删除资料)与Fixup(修正),” January 30, 2016. [Online]. Available: http://alrightchiu.github.io/SecondRound/red-black-tree-deleteshan-chu-zi-liao-yu-fixupxiu-zheng.html. [Accessed January 30, 2020].
最容易懂得红黑树
Sun_TTTT 于 2017-03-23 17:00:58 发布
介绍
红黑树是一个平衡的二叉树,但不是一个完美的平衡二叉树。虽然我们希望一个所有查找都能在~lgN次比较内结束,但是这样在动态插入中保持树的完美平衡代价太高,所以,我们稍微放松逛一下限制,希望找到一个能在对数时间内完成查找的数据结构。这个时候,红黑树站了出来。
阅读以下需要了解普通二叉树的插入以及删除操作。
红黑树是在普通二叉树上,对没个节点添加一个颜色属性形成的,同时整个红黑二叉树需要同时满足一下五条性质
红黑树需要满足的五条性质:
性质一:节点是红色或者是黑色;
在树里面的节点不是红色的就是黑色的,没有其他颜色,要不怎么叫红黑树呢,是吧。
性质二:根节点是黑色;
根节点总是黑色的,它不能为红。
性质三:每个叶节点(NIL或空节点)是黑色;
这个可能有点理解困难,可以看图:
这个图片就是一个红黑树,NIL节点是个空节点,并且是黑色的。
性质四:每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点);
就是连续的两个节点不能是连续的红色,连续的两个节点的意思就是父节点与子节点不能是连续的红色。
性质五:从任一节点到其没个叶节点的所有路径都包含相同数目的黑色节点;
还是看图:
从根节点到每一个NIL节点的路径中,都包含了相同数量的黑色节点。
这五条性质约束了红黑树,可以通过数学证明来证明,满足这五条性质的二叉树可以将查找删除维持在对数时间内。
当我们进行插入或者删除操作时所作的一切操作都是为了调整树使之符合这五条性质。
下面我们先介绍两个基本操作,旋转。
旋转的目的是将节点多的一支出让节点给另一个节点少的一支,旋转操作在插入和删除操作中经常会用到,所以要熟记。
下面是左旋和右旋
左旋:
右旋:
下面讲讲插入
我们先明确一下各节点的叫法
因为要满足红黑树的这五条性质,如果我们插入的是黑色节点,那就违反了性质五,需要进行大规模调整,如果我们插入的是红色节点,那就只有在要插入节点的父节点也是红色的时候违反性质四或者是当插入的节点是根节点时,违反性质二,所以,我们把要插入的节点的颜色变成红色。
下面是可能遇到的插入的几种状况:
1、当插入的节点是根节点时,直接涂黑即可;
2、当要插入的节点的父节点是黑色的时候。
这个时候插入一个红色的节点并没有对这五个性质产生破坏。所以直接插入不用在进行调整操作。
3、如果要插入的节点的父节点是红色且父节点是祖父节点的左支的时候。
这个要分两种情况,一种是叔叔节点为黑的情况,一种是叔叔节点为红的情况。
当叔叔为黑时,也分为两种情况,一种是要插入的节点是父节点的左支,另一种是要插入的节点是父亲的右支。
我们先看一下当要插入的节点是父节点的左支的情况:
这个时候违反了性质四,我们就需要进行调整操作,使之符合性质四,我们可以通过对祖父节点进行右旋同时将祖父节点和父节点的颜色进行互换,这样就变成了:
经过这样的调整可以符合性质四并且不对其他性质产生破坏。
当插入的节点是父节点的右支的时候:
当要插入的节点是父节点的右支的时候,我们可以先对父节点进行左旋,变成如下:
如果我们把原先的父节点看做是新的要插入的节点,把原先要插入的节点看做是新的父节点,那就变成了当要插入的节点在父节点的左支的情况,对,是的,就是按照当要插入的节点在父节点的左支的情况进行旋转,旋转完之后变成如下:
4、如果要插入的节点的父节点是红色且父节点是祖父节点的右支的时候;
这个时候的情况跟情况3所表述的情况是一个镜像,将情况3的左和右互换一下就可以了。
5、如果要插入的节点的父节点是红色并且叔叔节点也为红色,如下:
这个时候,只需将父亲节点和叔叔节点涂黑,将祖父节点涂红。
以上就是插入的全部过程。
下面我们再讲讲删除的操作:
首先你要了解普通二叉树的删除操作:
1.如果删除的是叶节点,可以直接删除;
2.如果被删除的元素有一个子节点,可以将子节点直接移到被删除元素的位置;
3.如果有两个子节点,这时候就可以把被删除元素的右支的最小节点(被删除元素右支的最左边的节点)和被删除元素互换,我们把被删除元素右支的最左边的节点称之为后继节点(后继元素),然后在根据情况1或者情况2进行操作。如图:
将被删除元素与其右支的最小元素互换,变成如下图所示:
然后再将被删除元素删除:
我们下面所称的被删除元素,皆是指已经互换之后的被删除元素。
加入颜色之后,被删除元素和后继元素互换只是值得互换,并不互换颜色,这个要注意。
下面开始讲一下红黑树删除的规则:
1.当被删除元素为红时,对五条性质没有什么影响,直接删除。
2.当被删除元素为黑且为根节点时,直接删除。
3.当被删除元素为黑,且有一个右子节点为红时,将右子节点涂黑放到被删除元素的位置,如图:
由
变成
4.当被删除元素为黑,且兄弟节点为黑,兄弟节点两个孩子也为黑,父节点为红,此时,交换兄弟节点与父节点的颜色;NIL元素是指每个叶节点都有两个空的,颜色为黑的NIL元素,需要他的时候就可以把它看成两个黑元素,不需要的时候可以忽视他。
如图:
由
变成:
5.当被删除元素为黑、并且为父节点的左支,且兄弟颜色为黑,兄弟的右支为红色,这个时候需要交换兄弟与父亲的颜色,并把父亲涂黑、兄弟的右支涂黑,并以父节点为中心左转。如图:
由
变成:
6.当被删除元素为黑、并且为父节点的左支,且兄弟颜色为黑,兄弟的左支为红色,这个时候需要先把兄弟与兄弟的左子节点颜色互换,进行右转,然后就变成了规则5一样了,在按照规则5进行旋转。如图:
由
先兄弟与兄弟的左子节点颜色互换,进行右转,变成:
然后在按照规则5进行旋转,变成:
7.当被删除元素为黑且为父元素的右支时,跟情况5.情况6 互为镜像。
8.被删除元素为黑且兄弟节点为黑,兄弟节点的孩子为黑,父亲为黑,这个时候需要将兄弟节点变为红,再把父亲看做那个被删除的元素(只是看做,实际上不删除),看看父亲符和哪一条删除规则,进行处理变化如图:
由:
变成:
8.当被删除的元素为黑,且为父元素的左支,兄弟节点为红色的时候,需要交换兄弟节点与父亲结点的颜色,以父亲结点进行左旋,就变成了情况4,在按照情况四进行操作即可,变化如下:
由:
交换兄弟节点与父亲结点的颜色,以父亲结点进行左旋 变成:
在按照情况四进行操作,变成:
好了,删除的步骤也讲完,没有讲到的一点就是,在添加删除的时候,时刻要记得更改根元素的颜色为黑。
这里并没有语言实现,只是讲了一下红黑树的插入删除步骤,你可以根据步骤自己把红黑树实现。
[点击这里]- 数据结构之红黑树插入与删除全程演示 梦醒潇湘 love 2013-01-23 11:15:08
http://blog.chinaunix.net/uid-26548237-id-3480169.html
已附本文后,照着规则一步一步的构建一个红黑树吧。
最后:
-
红黑树的实现其实是一个 2、3、4 树,只是将双节点或者三节点用红色进行了标示,如果你将红色节点放到和它父元素相同的高度,并把它和父元素看做是一个元素,你就会发现,变成了一个高度为 lgN 的二叉树,这个 2.3.4 树对红黑树很有启发意义。
-
上面的步骤其实可以不用死记硬背,是可以推导出来的,因为我们是把一个平衡但通过插入或者删除破坏了平衡的红黑树再次平衡,同过旋转让位,改变红黑颜色,使之符合那五条基本性质。比如遇到删除操作情况四的时候,我们可以把那个删除元素去除,发现左边比右边少一个黑元素,这个时候,怎么办,我们发现兄弟节点的子元素有一个红元素,操作这个不会影响那五条性质,所以我们通过变换颜色,旋转,即可让左右两边的的黑色数目一样。
-
旋转操作的目的是出让一个元素到另外的地方并且符合二叉树左小右大的性质,交换颜色的目的是为了保持红黑树的那五条性质。
-
要时刻记得 ,一切的操作都是为了保持那五条性质。
最后的最后,其实还有一种更为简单的红黑二叉树,这个简单的红黑二叉树实际上是一个 2.3 树,他只允许左节点为红节点,但是性能上肯定是不如这个红黑树。这个简单的红黑二叉树在《算法》第四版有介绍,掌握完之后再看这个简单的红黑二叉树,就会觉着简单 easy。
最后的最后的最后,一定要尝试着自己推导一下插入删除规则啊,不然经常忘,是睡一觉起来再看就有点懵逼的那种忘。
红黑树介绍
RWCC于 2022-05-28 10:30:00 发布
红黑树目录
-
红黑树的概念
-
红黑树的性质
-
红黑树节点的定义
-
红黑树结构
-
红黑树的插入操作
-
红黑树的验证
-
红黑树与 AVL 树的比较
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red 或 Black。 通过任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

红黑树的性质
-
每个结点不是红色就是黑色
-
根节点是黑色的
-
如果一个节点是红色的,则它的两个孩子结点是黑色的
-
对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
-
每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

我们分析一下:
最短路径为全黑,最长路径就是红黑节点交替(因为红色节点不能连续),每条路径的黑色节点相同,则最长路径、刚好是最短路径的两倍。
红黑树节点的定义
// 节点的颜色
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; // 节点的颜色
};
思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?
插入红色节点树的性质可能不会改变,而插入黑色节点每次都会违反性质4.
通过性质发现: 将节点设置为红色在插入时对红黑树造成的影响是小的,而黑色是最大的
总结:将红黑树的节点默认颜色设置为红色,是为尽可能减少在插入新节点对红黑树造成的影响。
红黑树结构
为了后续实现关联式容器简单,红黑树的实现中增加一个头结点,因为根节点必须为黑色,为了与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,pLeft域指向红黑树中最小的节点,_pRight域指向红黑树中最大的节点,如下:

红黑树的插入操作
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按照二叉搜索的树规则插入新节点
bool Insert(const T& value)
{
// 1. 按照二叉搜索树的规则插入新节点
// 空树
Node*& root = GetRoot();
if (nullptr == root)
{
root = new Node(value, BLACK);
root->_parent = _head;
}
else
{
// 非空
// 按照二叉搜索树的特性找待插入节点在树中的位置
Node* cur = root;
Node* parent = _head;
while (cur)
{
parent = cur;
if (value < cur->_value)
cur = cur->_left;
else if (value > cur->_value)
cur = cur->_right;
else
return false;
}
// 插入新节点
cur = new Node(value);
if (value < parent->_value)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
// 2. 检测新节点插入之后是否违反性质三:即是否存在红色节点连在一起的情况
// 因为新插入节点cur的颜色是红色的,如果cur双亲parent节点的颜色也是红色的
// 则违反了性质三
while (RED == parent->_color)
{
// 违反了性质三
Node* grandFather = parent->_parent;
// 此处grandFather一定不为空
// 因为:parent是红色的,则parent一定不是根节点,parent的双亲一定是存在的
if (parent == grandFather->_left)
{
// 课件中给的三种情况
Node* uncle = grandFather->_right;
if (uncle && RED == uncle->_color)
{
// 情况一:叔叔节点存在且为红
parent->_color = BLACK;
uncle->_color = BLACK;
grandFather->_color = RED;
cur = grandFather;
parent = cur->_parent;
}
else
{
// 叔叔节点为空 || 叔叔节点存在且为黑--->即情况二 或者 情况三
// 情况三
if (cur == parent->_right)
{
// 先对parent进行左单旋,然后将parent和cur交换---->变成情况二
RotateLeft(parent);
swap(parent, cur);
}
// 情况二:
// 将祖父和双亲节点的颜色交换,然后再对祖父树进行右单旋
grandFather->_color = RED;
parent->_color = BLACK;
RotateRight(grandFather);
}
}
else
{
// 课件总给的三种情况的反情况
}
}
}
// 需要更新_head的left和right指针域
_head->_left = MostLeft();
_head->_right = MostRight();
root->_color = BLACK;
return true;
}
-
检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur 为当前节点,p 为父节点,g 为祖父节点,u 为叔叔节点

解决方式:将 p,u 改为黑,g 改为红,然后把 g 当成 cur,继续向上调整
情况二: cur 为红,p 为红,g 为黑,u 不存在/u 为黑

p 为 g 的左孩子,cur 为 p 的左孩子,则进行右单旋转;相反,
p 为 g 的右孩子,cur 为 p 的右孩子,则进行左单旋转
p、g 变色–p 变黑,g 变红
情况三: cur为红,p为红,g为黑,u不存在/u为黑

p 为 g 的左孩子,cur 为 p 的右孩子,则针对 p 做左单旋转;相反,
p 为 g 的右孩子,cur 为 p 的左孩子,则针对 p 做右单旋转
则转换成了情况 2

针对每种情况进行相应的处理即可。
红黑树的验证
红黑树的检测分为两步:
-
检测其是否满足二叉搜索树(中序遍历是否为有序序列)
-
检测其是否满足红黑树的性质
bool IsValidRBTree()
{
PNode pRoot = GetRoot();
// 空树也是红黑树
if (nullptr == pRoot)
return true;
// 检测根节点是否满足情况
if (BLACK != pRoot->_color)
{
cout << "违反红黑树性质二:根节点必须为黑色" << endl;
return false;
}
// 获取任意一条路径中黑色节点的个数
size_t blackCount = 0;
PNode pCur = pRoot;
while (pCur)
{
if (BLACK == pCur->_color)
blackCount++;
pCur = pCur->_pLeft;
}
// 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
size_t k = 0;
return _IsValidRBTree(pRoot, k, blackCount);
}
bool _IsValidRBTree(PNode pRoot, size_t k, const size_t blackCount)
{
//走到null之后,判断k和black是否相等
if (nullptr == pRoot)
{
if (k != blackCount)
{
cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
return false;
}
return true;
}
// 统计黑色节点的个数
if (BLACK == pRoot->_color)
k++;
// 检测当前节点与其双亲是否都为红色
PNode pParent = pRoot->_pParent;
if (pParent && RED == pParent->_color && RED == pRoot->_color)
{
cout << "违反性质三:没有连在一起的红色节点" << endl;
return false;
}
return _IsValidRBTree(pRoot->_pLeft, k, blackCount) &&
_IsValidRBTree(pRoot->_pRight, k, blackCount);
}
红黑树与AVL树的比较
红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是 O (log2N),红黑树不追求绝对平衡,只需保证最长路径不超过最短路径的 2 倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树
小七mod已于 2023-11-04 14:49:41 修改
大家应该都学过平衡二叉树(AVLTree) ,了解到AVL树的性质,其实平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。AVL树的效率就是高在这个地方。如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建一颗平衡二叉树的成本其实不小. 这个时候就有人开始思考,并且提出了红黑树的理论,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。那么红黑树到底比AVL树好在哪里?
一、 红黑树简介
红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明,在当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。
二、为什么需要红黑树?
对于二叉搜索树,如果插入的数据是随机的,那么它就是接近平衡的二叉树,平衡的二叉树,它的操作效率(查询,插入,删除)效率较高,时间复杂度是 O(logN)。但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为 O (N),所以可以认为二叉搜索树的时间复杂度介于 O(logN)和 O (N) 之间,视情况而定。那么为了应对这种极端情况,红黑树就出现了,它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像 AVL 树的平衡因子的概念,它只是靠着满足红黑节点的 5 条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。
三、红黑树的特性
在讲解红黑树性质之前,先简单了解一下几个概念:
-
parent:父节点
-
sibling:兄弟节点
-
uncle:叔父节点( parent 的兄弟节点)
-
grand:祖父节点( parent 的父节点)
首先,红黑树是一个二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)。它同时满足以下特性:
-
节点是红色或黑色
-
根是黑色
-
叶子节点(外部节点,空节点)都是**黑色,**这里的叶子节点指的是最底层的空节点(外部节点),下图中的那些null节点才是叶子节点,null节点的父节点在红黑树里不将其看作叶子节点
-
红色
节点的子节点都是
黑色
-
红色节点的父节点都是黑色
-
从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点
-
-
从任一节点到叶子节点的所有路径都包含相同数目的黑色节点

根据上面的性质,我们来判断一下下面这课树是不是红黑树

上面这棵树首先很容易就能知道是满足性质1-4条的,关键在于第5条性质,可能乍一看好像也是符合第5条的,但实际就会陷入一个误区,直接将图上的最后一层的节点看作叶子节点,这样看的话每一条从根节点到叶子结点的路径确实都经过了3个黑节点。
但实际上,在红黑树中真正被定义为叶子结点的,是那些空节点,如下图。

这样一来,路径1有4个黑色节点(算上空节点),路径2只有3个黑色节点,这样性质5就不满足了,所以这棵树并不是一个红黑树节点。
注:下面的讲解图中将省略红黑树的null节点,请自行脑补
四、红黑树的效率
4.1 红黑树效率
红黑树的查找,插入和删除操作,时间复杂度都是O(logN)。
查找操作时,它和普通的相对平衡的二叉搜索树的效率相同,都是通过相同的方式来查找的,没有用到红黑树特有的特性。
但如果插入的时候是有序数据,那么红黑树的查询效率就比二叉搜索树要高了,因为此时二叉搜索树不是平衡树,它的时间复杂度O(N)。
插入和删除操作时,由于红黑树的每次操作平均要旋转一次和变换颜色,所以它比普通的二叉搜索树效率要低一点,不过时间复杂度仍然是O(logN)。总之,红黑树的优点就是对有序数据的查询操作不会慢到O(logN)的时间复杂度。
4.2 红黑树和 AVL 树的比较
-
AVL 树的时间复杂度虽然优于红黑树,但是对于现在的计算机,cpu 太快,可以忽略性能差异
-
红黑树的插入删除 \ 比 AVL 树更便于控制操作
-
红黑树整体性能略优于 AVL 树(红黑树旋转情况少于 AVL 树)
五、红黑树的等价变换

上面这颗红黑树,我们来将所有的红色节点上移到和他们的父节点同一高度上,就会形成如下结构

这个结构很明显,就是一棵**四阶\ **B\ **树(一个节点最多放三个数据)\ ,如果画成如下的样子大家应该就能看的更清晰了。

由上面的等价变换我们就可以得到如下结论:
-
红黑树 和 4阶B树(2-3-4树)具有等价性
-
黑色节点与它的红色子节点融合在一起,形成1个B树节点
-
红黑树的黑色节点个数 与 4阶B树的节点总个数相等
-
在所有的B树节点中,永远是黑色节点是父节点,红色节点是子节点。黑色节点在中间,红色节点在两边。
我们可以利用四阶B树与红黑树等价的性质,以红黑树转换成B树之后的节点情况来进行一个分类

六、红黑树的操作
红黑树的基本操作和其他树形结构一样,一般都包括查找、插入、删除等操作。前面说到,红黑树是一种自平衡的二叉查找树,既然是二叉查找树的一种,那么查找过程和二叉查找树一样,比较简单,这里不再赘述。相对于查找操作,红黑树的插入和删除操作就要复杂的多。尤其是删除操作,要处理的情况比较多,下面就来分情况讲解。
6 .1 旋转操作
在分析插入和删除操作前,先说明一下旋转操作,这个操作在后续操作中都会用得到。旋转操作分为左旋和右旋,左旋\ 是将某个节点旋转为其右孩子的左孩子,而右旋\ 是节点旋转为其左孩子的右孩子。这话听起来有点绕,所以还是请看下图:

上图包含了左旋和右旋的示意图,这里以右旋为例进行说明,右旋节点 M 的步骤如下:
-
将节点 M 的左孩子引用指向节点 E 的右孩子
-
将节点 E 的右孩子引用指向节点 M,完成旋转

旋转操作本身并不复杂,上面分析了右旋操作,左旋操作与此类似,只是右旋转的逆操作。
6.2 插入操作
红黑树的插入过程和二叉查找树插入过程基本类似,不同的地方在于,红黑树插入新节点后,需要进行调整,以满足红黑树的性质。
性质1规定红黑树节点的颜色要么是红色要么是黑色,那么在插入新节点时,这个节点应该是红色还是黑色呢?答案是红色,原因也不难理解。如果插入的节点是黑色,那么这个节点所在路径比其他路径多出一个黑色节点,这个调整起来会比较麻烦(参考红黑树的删除操作,就知道为啥多一个或少一个黑色节点时,调整起来这么麻烦了)。如果插入的节点是红色,此时所有路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的情况。这种情况下,通过变色和旋转进行调整即可,比之前的简单多了。所以插入的时候将节点设置为红色,可以保证满足性质 1、2、3、5 ,只有性质4不一定满足,需要进行相关调整。如果是添加根节点,则将节点设定为黑色。
6.2.1 插入操作的所有情况
我们在分析红黑树各种插入情况的时候,将其等价转换为 B 树,这样我们能够更直观的进行分类,首先确定几条性质:
-
B 树中,新元素必定是添加到叶子节点中(最底层的节点)
-
4 阶 B 树所有节点的元素个数 x 都符合 1 ≤ x ≤ 3

在上一章节红黑树的等价变换中,我们讲到了红黑树转换成B树总共有四种情况,也就是上图中叶子节点这四种情况,那么在我们进行插入操作的时候,会将节点插入到所有的叶子节点中,总共就会有12种情况,其中四种情况满足红黑树的性质,8种情况不满足红黑树性质。
6.2.1.1 满足红黑树性质 4
有 4 种情况满足红黑树的性质 4 **:parent\ ** 为黑色节点\ 。这四种情况不需要做任何额外的处理。

6.2.1.2 不满足红黑树性质 4
有 8 种情况不满足红黑树的性质 4 **:parent\ 为红色节点\ ( Double Red ),其中左面4种属于B树节点上溢\ 的情况(一个4阶B树节点中最多存放三个数,这四种情况本来已经有3个了,又插入了1个,变成了4个,超出了4阶B树节点的容量范围,这种情况称为上溢)。这八种情况需要进行额外的处理。

6.2.2 LL 和 RR 插入情况

如上图,插入52和60的位置分别是RR情况和LL情况。
**RR\ **情况\ :父节点为祖父节点的右节点,插入节点为父节点的右节点
**LL\ **情况\ :父节点为祖父节点的左节点,插入节点为父节点的左节点
这两种情况很明显,插入节点为红色,父节点也为红色,父节点的子节点为红色显然违背了红黑树的性质四,我们需要对这种情况进行修复,使其重新满足红黑树性质。
判定条件:uncle 不是红色节点。
这里的两种情况,他们的插入节点都是没有叔父节点的,所以叔父节点也不可能是红色。
案例修复:
我们在红黑树等价转换那一章节也讲过了,红黑树等价转换成B树之后,B树节点的中间节点(父节点)都是黑色,两边的节点(子节点)都是红色。但是上面两种情况插入后,插入位置的B树节点并不满足这个条件,所以我们对其进行修复,使其满足B树节点的条件之后,也就重新恢复了红黑树性质。
B树节点中的中间节点大小介于两个子节点之间。以上图RR情况为例,插入节点52的原父节点应该放在B树节点中间的位置,应当将其染成黑色。插入节点52的原祖父节点46,应当将其转换为插入节点原父节点的子节点,所以将其染成红色。LL情况同理
完成染色之后,需要对原祖父节点进行单旋操作,来进行父节点,子节点的重新分配。以上图为例:
-
RR情况应该原祖父节点46左旋,将插入节点的原父节点50旋转到中间的位置。
-
LL情况应当原祖父节点76右旋,将插入节点的原父节点72旋转到中间的位置。
修复之后的结果如下图:

修复步骤总结:
-
parent 染成黑色,grand 染成红色
-
grand 进行单旋操作
-
LL:右旋转
-
RR:左旋转
-
6.2.3 LR 和 RL 插入情况

如上图,插入48和74的位置分别是RL情况和LR情况。
RL 情况 :父节点为祖父节点的右节点,插入节点为父节点的左节点
LR 情况 :父节点为祖父节点的左节点,插入节点为父节点的右节点
这两种情况和上面的两种情况一样,插入节点为红色,父节点也为红色,父节点的子节点为红色显然违背了红黑树的性质四,我们需要对这种情况进行修复,使其重新满足红黑树性质。
判定条件:uncle 不是红色节点。
这两种情况的插入节点也是没有叔父节点的。
案例修复:
B树节点中的中间节点大小介于两个子节点之间。以上图RL情况为例,插入节点48大小介于原父节点和原祖父节点之间,它应该是B树节点中的中间节点,所以将插入节点48染成黑色,将原祖父节点46染成红色来作为插入节点的子节点。LR情况同理
完成染色之后,需要进行双旋操作,来进行父节点,子节点的重新分配。以上图为例:
-
RL情况应该原父节点50右旋,将插入节点48上移到原父节点50的高度,然后将插入节点的原祖父节点46进行左旋,将插入节点48移动到中间位置,成为中间节点。
-
LR情况应该原父节点72左旋,将插入节点74上移到原父节点72的高度,然后将插入节点的原祖父节点76进行右旋,将插入节点74移动到中间位置,成为中间节点。
修复之后的结果如下图:

修复步骤总结:
-
插入节点染成黑色,grand 染成红色
-
进行双旋操作
-
LR:parent 左旋转, grand 右旋转
-
RL:parent 右旋转, grand 左旋转
-
6.2.4 上溢的 LL 插入情况

如上图,插入10的位置是上溢的LL情况。
溢 LL 情况:父节点为祖父节点的左节点,插入节点为父节点的左节点。并且构成的新的B树节点已经超过了B树节点容量大小范围。
这种情况和之前非上溢的四种情况一样,插入节点为红色,父节点也为红色,父节点的子节点为红色显然违背了红黑树的性质四,我们需要对这种情况进行修复,使其重新满足红黑树性质。
判定条件:uncle 是红色节点。满足这个条件的就都是上溢的情况,上溢的修复只需要染色,不需要旋转。
案例修复:
像这种上溢的情况,就需要从溢出的 B 树节点中选出一个节点进行向上合并,选择 B 树节点中中间的树去进行向上合并,这里中间的两个节点就是原父节点 17 和原祖父节点 25,选这两个哪一个向上合并都是对的,但是我们最好选择以后方便操作的,很显然,应该选择原祖父节点 25 来进行向上合并,因为向上合并就是和最上层的 38 和 55 来组合成新的 B 树节点,向上合并的节点肯定是一个子节点,需要与上层相连,而原祖父节点 25 本身就已经和上层连接了,相对更加方便后续的操作。原祖父节点向上合并后,将其染成红色。
原祖父节点 25 向上合并后,它原来左右两边的节点需要分裂成两个子树,也就是原父节点 17 和插入节点 10 形成一个子树,原叔父节点 33 形成一个子树。这两个分裂形成的树都是以后 25 的子树。左边的子树由原父节点作为中间节点,染成黑色,右边的子树由原叔父节点作为中间节点,染成黑色。
修复之后的结果如下图:

修复步骤总结:
-
parent、uncle 染成黑色
-
grand 向上合并
-
将向上合并的 grand 染成红色,相对上一层,就当做是新添加的节点,再次来一遍插入情况的判断,进行处理。
grand 向上合并时,可能继续发生上溢。这种情况就继续递归调用修复方法就可以了。若上溢持续到根节点,只需将根节点染成黑色即可(这个意思就是说断向上上溢,一直上溢到了B树的根节点位置了,只需要将向上合并的节点变成黑色作为红黑树的根节点即可。因为从B树根节点选择出来上溢的节点,肯定就是作为整个红黑树的根节点了)。
6.2.5 上溢的 RR 插入情况

如上图,插入36的位置是上溢的RR情况。
上溢 RR 情况:父节点为祖父节点的右节点,插入节点为父节点的右节点。并且构成的新的B树节点已经超过了B树节点容量大小范围。
判定条件:uncle 是红色节点
案例修复:
上溢 RR 情况的修复,和上溢 LL 情况基本一致,只是修复的位置不同,这里中间的两个节点就是原父节点 33 和原祖父节点 25,选择原祖父节点 25 来进行向上合并,原祖父节点向上合并后,将其染成红色。
原祖父节点 25 向上合并后,它原来左右两边的节点需要分裂成两个子树,也就是原父节点 33 和插入节点 36 形成一个子树,原叔父节点 17 形成一个子树。这两个分裂形成的树都是以后 25 的子树。左边的子树由原叔父节点作为中间节点,染成黑色,右边的子树由原父节点作为中间节点,染成黑色。
修复之后的结果如下图:

修复步骤总结:
-
parent、uncle 染成黑色
-
grand 向上合并
- 染成红色(其实染成红色就已经是完成了向上合并,因为祖父节点和祖父节点的父节点的连接指向并没有变),当做是新添加的节点进行处理
6.2.6 上溢的 LR 插入情况

如上图,插入20的位置是上溢的LR情况。
上溢 LR情况:父节点为祖父节点的左节点,插入节点为父节点的右节点。并且构成的新的B树节点已经超过了B树节点容量大小范围。
判定条件:uncle 是红色节点
案例修复:
上溢 LR 情况的修复,和其他上溢情况基本一致,只是修复的位置不同,这里中间的两个节点就是原父节点 17 和原祖父节点 25,选择原祖父节点 25 来进行向上合并,原祖父节点向上合并后,将其染成红色。
原祖父节点 25 向上合并后,它原来左右两边的节点需要分裂成两个子树,也就是原父节点 17 和插入节点 20 形成一个子树,原叔父节点 33 形成一个子树。这两个分裂形成的树都是以后 25 的子树。左边的子树由原父节点作为中间节点,染成黑色,右边的子树由原叔父节点作为中间节点,染成黑色。
修复之后的结果如下图:

修复步骤总结:
-
parent、uncle 染成黑色
-
grand 向上合并
- 染成红色,当做是新添加的节点进行处理
6.2.7 上溢的 RL 插入情况

如上图,插入30的位置是上溢的RL情况。
上溢 RL 情况:父节点为祖父节点的右节点,插入节点为父节点的左节点。并且构成的新的B树节点已经超过了B树节点容量大小范围。
判定条件:uncle 是红色节点
案例修复:
上溢 RL 情况的修复,和其他上溢情况基本一致,只是修复的位置不同,这里中间的两个节点就是原父节点 33 和原祖父节点 25,选择原祖父节点 25 来进行向上合并,原祖父节点向上合并后,将其染成红色。
原祖父节点 25 向上合并后,它原来左右两边的节点需要分裂成两个子树,也就是原父节点 33 和插入节点 30 形成一个子树,原叔父节点 17 形成一个子树。这两个分裂形成的树都是以后 25 的子树。左边的子树由原叔父节点作为中间节点,染成黑色,右边的子树由原父节点作为中间节点,染成黑色。
修复之后的结果如下图:

修复步骤总结:
-
parent、uncle 染成黑色
-
grand 向上合并
- 染成黑色,当做是新添加的节点进行处理
6.2.8 插入情况总结
插入一共有12种情况:
-
插入节点的父节点是黑色的情况有4种
这种情况仍然会维持红黑树的性质,则不需要进行额外处理。
-
插入节点的父节点是红色的情况有8种
这种情况不满足红黑树的性质4,需要进行额外的修复处理。
这8种情况中:
-
叔父节点不是红色的情况有4种
这些情况都是非上溢,需要通过重新染色和旋转来进行修复
-
叔父节点是红色的情况有4种
这些情况都是上溢的,只需要通过祖父节点上溢合并和染色即可完成修复
-
6.3 删除 操作
相较于插入操作,红黑树的删除操作则要更为复杂一些。B树中,最后真正被删除的元素都在叶子节点中。所以在红黑树中,被删除的节点一定也在最后一层。

6.3.1 删除 操作的所有情况
上面我们说删除节点一定都在最后一层,最后一层有红色节点和黑色节点,我们就以删除节点的颜色来区分删除操作的所有情况。
6.3.1.1 删除红色节点
如果删除的节点是红色直接删除,不用作任何调整。因为删除最后一层的红色节点,并没有影响红黑树的任何性质。

6.3.1.2 删除黑色节点
有 3 种情况:
-
拥有 2 个红色子节点的黑色节点
- 不可能被直接删除,因为会找它的子节点替代删除,因此不用考虑这种情况
-
拥有 1 个红色子节点的黑色节点
-
黑色叶子节点

6.3.2 删除拥有 1 个 红色 子节点的 黑色 节点

删除拥有1个红色子节点的黑色节点的情况,是需要我们做相关的处理的。这里删除的就是节点46和76,他们只有一个红色子节点。
对于一个二叉树来说,删除一个度为1的节点(度指的是一个节点的子节点个数),将其删除后需要用它唯一的子节点来进行替换。而红黑树的这种情况的判定条件,就是判定要替代删除节点的子节点是不是红色
判定条件:用以替代的子节点是红色节点
案例修复:

删除黑色节点46和76
第一步:

将 46 与父节点的连接断开
第二步:

46 唯一的红色子节点50作为代替 46 的节点,将其与 46 的父节点进行连接
第三步:

断开 46 与 50 的连接,将 46 删除
删除节点 76 的过程与删除节点 46 相同
第一步:

第二步:

第三步:

但是现在我们发现,80 是红色节点,它的子节点72还是红色节点,这样明显不符合红黑树的性质,还需要进一步修复。

将替代的子节点染成黑色即可保持红黑树性质,修复完成
修复步骤总结:
-
用删除节点的唯一子节点对其进行替代
-
将替代节点染成黑色
6.3.3 删除黑色叶子节点 —— 删除节点为根节点
一棵红黑树只有一个黑色根节点(也就是唯一的一个叶子节点,整个红黑树只有这一个黑色节点),可直接删除该节点,无需做其他操作。
6.3.4 删除黑色叶子节点 —— 删除节点的兄弟节点为黑色
讲这种删除情况前先举一个例子

上面这个我们要删除节点88,该节点为黑色叶子节点,它的兄弟节点是黑色76。从B树的角度来看,如果删除88,因为四阶B树的节点中最少存有1个元素,如果不足,则会造成**下溢\ 。也就是需要从88的兄弟节点中借一个子节点出来。这就是这一节我们讨论的删除情况的核心修复思想。
6.3.4.1 兄弟节点至少有 1 个 红色 子节点
下面三个图分别对应着兄弟节点至少有一个红色子节点的三种情况。删除节点为88,为黑色叶子节点,它的兄弟节点是76,为黑色。兄弟节点76都至少有一个红色子节点,三种情况分别为76拥有一个红色右子节点,76拥有一个红色左子节点,76拥有两个红色子节点。因为兄弟节点有红色子节点,所以可以借出一个节点来进行修复。

这三种情况,黑色叶子节点被删除后,会导致B树节点下溢(比如删除88),就可以从兄弟节点中借出一个红色子节点来进行修复。
判定条件:兄弟节点至少有 1 个红色子节点
案例修复:
1、兄弟节点有一个右子节点:

先将88节点删除

删掉之后,从B树的角度来看就出现了下溢,这个时候就需要父节点下来,在兄弟节点的子结点中找一个,将他升上去代替。具体的实现就是要对节点进行旋转。

我们可以看出,80、76、78组成的树是一个LR的情况,先对76进行左旋转(可以将76看作父节点),这样78就上去了,再对80进行右旋转(可以将80看成祖父节点),80就下去了。

旋转完了之后,如上图。将旋转完之后的中心节点(就是78、76、80组成的树的最中心的节点,这里就是78)进行重新染色,继承删除节点的父节点80的颜色。最后再将78、76、80组成的树的左右两个节点染成黑色即可完成修复。
2、兄弟节点有一个左子节点:

先将88节点删除

删掉之后,从B树的角度来看就出现了下溢,这个时候就需要父节点下来,在兄弟节点的子结点中找一个,将他升上去代替。具体的实现就是要对节点进行旋转。

我们可以看出,80、76、72组成的树是一个LL的情况,直接对80进行右旋(将80看成是祖父节点)。

旋转完了之后,如上图。将旋转完之后的中心节点(就是76、72、80组成的树的最中心的节点,这里就是76)进行重新染色,继承删除节点的父节点80的颜色。最后再将76、72、80组成的树的左右两个节点染成黑色即可完成修复。
3、兄弟节点有两个左右子节点:

先将88节点删除

删除之后,其实可以有两种旋转可以进行修复,既可以使用LL方式进行旋转,也可以使用LR方式进行旋转。但是因为LL方式只需要旋转一次,我们就选用LL方式。

直接对80进行右旋

旋转完了之后,如上图。将旋转完之后的中心节点(就是78、72、76、80组成的树的最中心的节点,这里就是76)进行重新染色,继承删除节点的父节点80的颜色。最后再将78、72、76、80组成的树的左右两个节点染成黑色即可完成修复。
修复步骤总结:
-
进行旋转操作
-
旋转之后的中心节点继承父节点(删除节点的父节点)的颜色
-
旋转之后的左右节点染为黑色
6.3.4.2 兄弟节点没有红色子节点
当删除节点的兄弟节点没有红色节点可以借出的情况下,就需要父节点来向下合并进行修复,父节点向下和兄弟节点合并成新的B树节点来解决下溢。
判定条件:兄弟节点没有1个红色子节点
案例修复:
1、父节点为红色:

删除节点88,出现下溢

因为兄弟节点76没有可以借出的红色节点,所以需要父节点80来向下与76合并进行修复

将兄弟节点76染成红色,父节点80染成黑色即可完成修复
2、父节点为黑色:

删除节点88,删除之后节点88就会出现下溢

删除之后父节点80应该向下合并进行修复,但是因为父节点80为黑色,如果向下合并之后,其实就相当于80这个节点也出现了下溢。

这个时候只需要把父节点当作被删除的节点进行处理即可
修复步骤总结:
-
父节点向下与兄弟节点进行合并
-
将兄弟染成红色、父节点染成黑色即可修复红黑树性质
- 如果父节点是黑色,直接将父节点当成被删除的节点处理,来修复父节点的下溢情况
6.3.5 删除黑色叶子节点 —— 删除节点的兄弟节点为红色

如果删除节点的兄弟节点为红色,这样删除节点出现下溢后没办法通过兄弟节点来进行修复。这就需要先把红黑树转换为兄弟节点为黑色的情况,就可以套用上面讲的修复方法来进行修复了。
判定条件:兄弟节点是红色
案例修复:

删除88节点之前,需要先转换成兄弟节点为黑色的情况,当前88的兄弟节点是红色55。可以将其看作LL情况,对父节点88进行右旋转,这样55就被移动上去了,成了80的父节点。76也被移动上去了,成了80的子节点。

这种情况,删除节点88的兄弟节点就变成了黑色,并且没有红色子节点,可以继续套用之前讲的方法来进行修复了。

删除掉88,将80染成黑色,76染成红色,完成修复。
修复步骤总结:
-
兄弟节点染成 BLACK,父节点染成染成 RED,对父节点进行右旋
-
于是又回到兄弟节点是黑色的情况(侄子节点变为兄弟节点),继续使用兄弟节点为黑色的方法进行修复
七、红黑树的平衡
AVL是靠平衡因子来保持平衡的,比如平衡因子为1,那么左右子树的高度差就不能超过1,是一种强平衡。
对于红黑树而言,为何那5条性质,就能保证红黑树是平衡的?
- 因为那5条性质,可以保证红黑树等价于4阶B树

B树比较矮,它本身就是平衡的,高度越小越平衡。
红黑树就是能保证这个树高度不会特别高,红黑树的最大高度是 2 ∗ log2(n + 1) ,依然是 O(logn) 级别,因为高度不会很大进而维持一种相对平衡的状态。相比AVL树,红黑树的平衡标准比较宽松**:没有一条路径会大于其他路径的\ **2\ **倍\ 。这是是一种弱平衡、黑高度平衡(黑高度只算黑色节点个数,红黑树的任何一条路径的黑色节点数一样,则黑高度都是一样)。
八、红黑树的平均时间复杂度
-
搜索:O(logn)
-
添加:O(logn),O(1) 次的旋转操作
-
删除:O(logn),O(1) 次的旋转操作
九、 AVL 树 vs 红黑树
9.1 AVL 树
-
平衡标准比较严格:每个左右子树的高度差不超过 1
-
最大高度是 1.44 × log 2 n + 2 − 1.328 1.44 \times \log_2 n + 2 - 1.328 1.44×log2n+2−1.328(100W 个节点,AVL 树最大树高 28)
-
搜索、添加、删除都是 O ( log n ) O(\log n) O(logn) 复杂度,其中添加仅需 O ( 1 ) O(1) O(1) 次旋转调整、删除最多需要 O ( log n ) O(\log n) O(logn) 次旋转调整
9.2 红黑树
-
平衡标准比较宽松:没有一条路径会大于其他路径的 2 倍
-
最大高度是 2 × log 2 ( n + 1 ) 2 \times \log_2(n + 1) 2×log2(n+1)(100W 个节点,红黑树最大树高 40)
-
搜索、添加、删除都是 O ( log n ) O(\log n) O(logn) 复杂度,其中添加、删除都仅需 O ( 1 ) O(1) O(1) 次旋转调整
9.3 如何选择
-
搜索的次数远远大于插入和删除,选择 AVL 树;搜索、插入、删除次数几乎差不多,选择红黑树
-
相对于 AVL 树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于 AVL 树
-
红黑树的平均统计性能优于 AVL 树,实际应用中更多选择使用红黑树
9.4 案例对比
10, 35, 47, 11, 5, 57, 39, 14, 27, 26, 84, 75, 63, 41, 37, 24, 96组成一棵树
9.4.1 二叉搜索树

非常不平衡
9.4.2 AVL 树

最平衡
9.4.3 红黑树

相对比较平衡
深入解析红黑树(RB-Tree):原理、操作及应用
无敌岩雀已于 2024-08-31 20:05:41 修改
文章目录
-
一、红黑树的特点与性质
-
二、红黑树的实现
-
-
1、实现红黑树的插入操作
-
2、红黑树的验证方法
-
-
a. Check 函数
-
b. IsBalance 函数
-
-
红黑树作为一种自平衡的二叉搜索树,在计算机科学领域中占据着重要的地位。它的设计旨在在维持树的平衡性的同时,保证各种操作的时间复杂度始终稳定在较低水平。红黑树的灵活性和高效性使得它成为众多编程语言和系统实现中不可或缺的数据结构之一。本文将带领读者深入探究红黑树的结构与原理。(c++实现)
一、红黑树的特点与性质
- 红黑树的定义与性质
红黑树是一种特殊的自平衡二叉查找树,它通过颜色和一系列性质来确保树的平衡,从而实现高效的查找、插入和删除操作。在红黑树中,每个节点都被赋予红色或黑色的颜色,并且这些颜色与树的五个基本性质一起工作,共同维持树的平衡。
红黑树是具有着色性质的二叉搜索树:
-
每个节点要么是红色,要么是黑色。
-
根节点是黑色。
-
每个叶子节点(通常称为NIL或空节点)是黑色。在大多数实现中,叶子节点不实际存储,而是用NIL或空指针表示。
-
如果一个节点是红色的,则它的两个子节点都是黑色的(即,不能有两个相邻的红色节点)。
-
对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
这些性质共同确保了红黑树的平衡性。特别是性质5,它树确保没有一条路径会比其他路径长出两倍。这种平衡性保证了红黑树在查找、插入和删除操作中的时间复杂度都是 O(log n),其中n是树中节点的数量。着色法则的一个推论是,红黑树的高度最多是 2log(n+1) 。因此,查找保证是一种对数级别的操作。

有 n 个节点的红黑树的高度最多是 2log(n+1),这是为什么呢?
首先我们来我们考虑一个问题,根节点为黑高为 h 的红黑树,内部节点个数至少有多少个?(黑高:从某节点出发(不含该节点)到达任意一叶节点的路径上黑节点的总数。)
内部节点上最少的情况就是总共 h 层黑节点的满树形态。若根节点黑高为 h,内部节点数最少有 2h-1 个。因此,若红黑树总高度为 h ,则根节点的黑高肯定是大于等于 h/2 的,因此内部节点数 n >= 2h/2-1,由此推出 h <= 2log(n+1) 。
- 红黑树与AVL树的对比
红黑树同AVL树类似,也是一种自平衡的二叉搜索树,它通过颜色和一系列性质来维护树的平衡性。与AVL树不同的是,红黑树并不追求完全的平衡,而是允许局部的不平衡,这使得红黑树在插入或删除节点时的旋转操作次数较少,降低了操作的复杂性。同时,红黑树的查找、插入和删除操作的时间复杂度也能保持在O(log n)。由于红黑树的这种特性,它在需要频繁进行插入和删除操作的应用场景中表现得更好。
总的来说,红黑树在平衡性、操作复杂性和性能之间找到了一个较好的平衡点,使得它在许多实际应用中成为了一个优秀的选择。而AVL树虽然具有严格的平衡性,但由于其操作的复杂性以及在实际应用中的效率问题,使得它的使用范围相对较小。普通二叉搜索树虽然简单,但在数据动态变化的情况下,其性能可能会受到影响。因此,在选择使用哪种树形结构时,需要根据具体的应用场景和需求进行权衡。 深入
二、红黑树的实现
- 定义红黑树节点的结构体
在此我们定义一个模板结构体RBTreeNode,它是红黑树中的一个节点。红黑树是一种自平衡的二叉搜索树,其中每个节点都带有颜色属性(红色或黑色)。Colour的枚举类型,它有两个值:RED和BLACK。这个枚举类型用于表示红黑树中节点的颜色:
enum Colour { RED, BLACK };// 节点的颜色
template<class K, class V>
struct RBTreeNode{
RBTreeNode<K, V>* _left; // 节点的左孩子
RBTreeNode<K, V>* _right; // 节点的右孩子
RBTreeNode<K, V>* _parent; // 节点的父节点
pair<K, V> _kv; // 节点的键值对
Colour _col; // 节点的颜色
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)
{}
};
_left、_right和_parent都初始化为nullptr,表示新节点最初没有子节点或父节点。 _kv初始化为传入的键值对kv。 _col初始化为RED,表示新创建的节点默认是红色的。
为什么要默认是红色呢?
红黑树的困难在于将一个新的值插入到树中,通常把新的值作为树叶放到树中。如果插入时把该节点作为黑色,那么一定违反性质5,因为会建立一条更长的黑节点的路径。因此,该节点必须涂成红色。因此我们在构造函数里将其颜色初始化为红色。
template<class K,class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
public:
//...
private:
Node* _root = nullptr;
};
1、实现红黑树的插入操作
在找到插入位置时,如果父节点是黑的,插入完成,因为插入红色节点不会违反性质5。如果它的父节点已经是红色的,那么我们得到连续红色节点,这就违反性质4。在这种情况下,我们必须调整该树以确保性质4满足(且不引起性质5被破坏)。因此我们在插入时需要的基本操作是节点颜色的改变以及树的旋转:
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);
if (parent->_kv.first < kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
-
修复红黑树性质:插入新节点后,红黑树可能不再满足其性质。因此,我们需要进行一系列操作来修复这些性质。这主要包括检查新节点的父节点和叔叔节点的颜色,并根据需要执行旋转和重新着色操作。
关于旋转操作我们在 AVL 树中已进行描述,若仍存疑,可以点击此处(深入探索AVL树:优雅的自平衡二叉搜索树_avl树平衡的方法 https://blog.youkuaiyun.com/weixin_73494835/article/details/136660341)。
-
检查父节点颜色:如果新节点的父节点是黑色,那么插入操作不会破坏红黑树的任何性质,因此无需进行任何修复操作。如下图插入10或20,不会破坏红黑树性质:

-
父节点和叔叔节点均为红色:如果新节点的父节点是红色,并且其叔叔节点(父节点的兄弟节点)也是红色,那么我们需要将父节点和叔叔节点都重新着色为黑色,并将祖父节点(父节点的父节点)着色为红色。然后,我们将祖父节点作为新的当前节点,继续向上检查,直到根节点或遇到黑色父节点为止。如图所示:

分为两种对称情况,图示为插入30。若插入3,同理即可。代码实现如下:
while (parent && parent->_col == RED) { if (parent == grandparent->_left) { Node* uncle = grandparent->_right; if (uncle && uncle->_col == RED) { parent->_col = uncle->_col = BLACK; grandparent->_col = RED; cur = grandparent; parent = cur->_parent; } else { //.... } } else { Node* uncle = grandparent->_left; if (uncle && uncle->_col == RED) { parent->_col = uncle->_col = BLACK; grandparent->_col = RED; cur = grandparent; parent = cur->_parent; } else { //... } } }此段代码用于处理父节点和叔叔节点都为红色的情况:
parent是新插入节点的父节点,grandparent是parent的父节点,_col是节点的颜色(RED 或 BLACK)的属性,_left和_right是指向左右子节点的指针。这段代码的逻辑是这样的:
-
while (parent && parent->_col == RED):这个循环会持续进行,直到parent为空(即已经到达根节点)或者parent的颜色为黑色。 -
if (parent == grandparent->_left):判断parent是否是grandparent的左子节点。 -
Node* uncle = grandparent->_right;:如果是左子节点,则叔叔节点(uncle)是grandparent的右子节点。 -
if (uncle && uncle->_col == RED):如果叔叔节点存在且为红色,那么执行以下操作:
-
parent->_col = uncle->_col = BLACK;:将父节点和叔叔节点的颜色都改为黑色。 -
grandparent->_col = RED;:将祖父节点的颜色改为红色。 -
cur = grandparent;和parent = cur->_parent;:更新cur为祖父节点,并将parent更新为cur的父节点,以便在下一次循环中继续向上检查。
-
-
如果叔叔节点不存在或者不是红色,代码进入
else块,这里通常包含一些旋转和颜色调整的逻辑,我们将在下面继续讨论。
我们给出抽象图,注意对称情况,我们不在赘述:

如果这次更新完后,祖父节点作为了新节点。若它的父节点为红,则继续向上更新。
-
-
父节点为红色,叔叔节点不存在或为黑色:在这种情况下,我的当前的节点称为新节点(下文统一称其为新节点),因此该节点可能是由其子树变化得来,也有可能是新插入的节点。我们需要进行旋转操作。具体的旋转方式取决于新节点是父节点的左孩子还是右孩子。如果新节点是父节点的左孩子,并且父节点是其祖父节点的左孩子,则对祖父节点进行右旋;如果新节点是父节点的右孩子,并且父节点是其祖父节点的右孩子,则对祖父节点进行左旋。旋转后,我们将原父节点的颜色更改为黑色,并将祖父节点的颜色更改为红色。如图所示:

分为两种对称情况,图示为插入2。若插入55,同理即可。
如果新节点是父节点的右孩子,并且父节点是其祖父节点的左孩子,则先对父亲节点进行左旋,再对祖父节点进行右旋。如果新节点是父节点的左孩子,并且父节点是其祖父节点的右孩子,则先对父亲节点进行右旋,再最祖父节点进行左旋。旋转后,我们将原祖父节点的颜色更改为红色,并将新节点的颜色更改为黑色。如图所示:

同样分为两种对称情况,图示为插入4。若插45,先左单旋父节点,再右单旋祖父节点。
⚠️需要注意的是:
如果叔叔节点如果不存在,那么新节点一定是新插入的节点。
因为如果新节点不是新插入节点,那么父亲节点和新节点一定由一个节点的颜色是黑色的,不满足性质5:每条路径黑色节点个数相同。
如果叔叔节点存在,根据判断条件,叔叔是黑色的。那么新节点原来的颜色肯定是黑色。变为红色的原因是因为新节点的子树在调整过程中改变了新节点的颜色。
代码实现如下:
while (parent && parent->_col == RED) { Node* grandparent = parent->_parent; if (parent == grandparent->_left) { Node* uncle = grandparent->_right; if (uncle && uncle->_col == RED) { //... 叔叔存在且为红 } else { //情况二:叔叔不存在 或者 存在但为黑色 //旋转+变色 if (cur == parent->_left) { // g // p u // c RotateR(grandparent); parent->_col = BLACK; grandparent->_col = RED; } else { // g // p u // c RotateL(parent); RotateR(grandparent); cur->_col = BLACK; grandparent->_col = RED; } break; } } else { Node* uncle = grandparent->_left; if (uncle && uncle->_col == RED) { //... 情况一:叔叔存在且为红 } else { // 情况二:叔叔不存在或者存在且为黑 // 旋转+变色 // g // u p // c if (cur == parent->_right) { RotateL(grandparent); parent->_col = BLACK; grandparent->_col = RED; } else { // g // u p // c RotateR(parent); RotateL(grandparent); cur->_col = BLACK; grandparent->_col = RED; } break; } } -
-
如果当前节点
cur是其父节点的左孩子,并且父节点是祖父节点的左孩子,则执行右旋操作RotateR(grandparent)。这会将父节点移动到祖父节点的右子树,并相应地更新子节点的父节点指针。旋转后,将父节点变为黑色,祖父节点变为红色。 -
如果当前节点
cur是其父节点的右孩子,并且父节点是祖父节点的左孩子,则首先执行左旋操作RotateL(parent),将当前节点cur移动到父节点的左子树。接着执行右旋操作RotateR(grandparent),将父节点移动到祖父节点的右子树。旋转后,将当前节点cur变为黑色,祖父节点变为红色。
类似地,当父节点是祖父节点的右孩子时,执行相反的操作:
-
如果当前节点
cur是其父节点的右孩子,并且父节点是祖父节点的右孩子,则执行左旋操作RotateL(grandparent)。 -
如果当前节点
cur是其父节点的左孩子,并且父节点是祖父节点的右孩子,则首先执行右旋操作RotateR(parent),接着执行左旋操作RotateL(grandparent)。
我们分别给出新节点与父亲在不同方向的抽象图和相同方向的抽象图,注意对称情况:

如果新节点是父节点的右孩子,并且父节点是其祖父节点的左孩子,则先对父亲节点进行左旋,再最祖父节点进行右旋。如果新节点是父节点的左孩子,并且父节点是其祖父节点的右孩子,则先对父亲节点进行右旋,再最祖父节点进行左旋。

如果新节点是父节点的左孩子,并且父节点是其祖父节点的左孩子,则对祖父节点进行右旋;如果新节点是父节点的右孩子,并且父节点是其祖父节点的右孩子,则对祖父节点进行左旋。
此次旋转完后,我们就结束循环,此时可以确保树仍然满足红黑树的定义。
-
确保根节点为黑色:在所有修复操作完成后,我们还需要确保根节点是黑色的。如果根节点在修复过程中被着色为红色,我们需要将其重新着色为黑色。在找到插入位置时,如果父节点是黑的,插入完成,因为插入红色节点不会违反性质5。如果它的父节点已经是红色的,那么我们得到连续红色节点,这就违反性质4。在这种情况下,我们必须调整该树以确保性质4满足(且不引起性质5被破坏)。因此我们在插入时需要的基本操作是节点颜色的改变以及树的旋转:
_root->_col = BLACK;
通过执行上述步骤,我们可以确保在插入新节点后,红黑树仍然保持其平衡性质,并且能够有效地支持查找、删除等后续操作。红黑树的插入操作在平均情况下的时间复杂度为O(log n),其中n为树中节点的数量。
插入的完整代码如下:
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);
if (parent->_kv.first < kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
while (parent && parent->_col == RED) {
Node* grandparent = parent->_parent;
if (parent == grandparent->_left) {
Node* uncle = grandparent->_right;
//情况一 :叔叔存在且为红
if (uncle && uncle->_col == RED) {
//先变色 后向上处理
parent->_col = uncle->_col = BLACK;
grandparent->_col = RED;
cur = grandparent;
parent = cur->_parent;
}
else {
//情况二:叔叔不存在 或者 存在但为黑色
//旋转+变色
if (cur == parent->_left) {
// g
// p u
// c
RotateR(grandparent);
parent->_col = BLACK;
grandparent->_col = RED;
}
else {
// g
// p u
// c
RotateL(parent);
RotateR(grandparent);
cur->_col = BLACK;
grandparent->_col = RED;
}
break;
}
}
else {
Node* uncle = grandparent->_left;
// 情况一:叔叔存在且为红
if (uncle && uncle->_col == RED) {
// 变色
parent->_col = uncle->_col = BLACK;
grandparent->_col = RED;
// 继续往上处理
cur = grandparent;
parent = cur->_parent;
}
else
{
// 情况二:叔叔不存在或者存在且为黑
// 旋转+变色
// g
// u p
// c
if (cur == parent->_right) {
RotateL(grandparent);
parent->_col = BLACK;
grandparent->_col = RED;
}
else {
// g
// u p
// c
RotateR(parent);
RotateL(grandparent);
cur->_col = BLACK;
grandparent->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
2、红黑树的验证方法
红黑树也是一种特殊的二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断该二叉树是否满足二叉搜索树的性质:
void _InOrder(Node* root){
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << endl;
_InOrder(root->_right);
}
void InOrder() { _InOrder(_root); }
我们也要检测其是否满足红黑树的性质,我们需要两个函数:
a. Check 函数
Check 函数用于递归地检查红黑树的性质是否得到满足。它接受三个参数:当前节点 cur、从根节点到当前节点路径上的黑色节点数 blackNum 和从根节点到最左侧路径上的黑色节点数 refBlackNum。
-
空节点检查:如果当前节点为空(
cur == nullptr),则检查从根节点到该路径的黑色节点数blackNum是否与最左侧路径的黑色节点数refBlackNum相等。如果不相等,则输出错误信息并返回false。 -
连续红色节点检查:如果当前节点为红色且其父节点也为红色,则违反了红黑树的性质(性质 4),输出错误信息并返回
false。 -
黑色节点计数:如果当前节点为黑色,则增加
blackNum的计数。 -
递归检查子节点:递归调用
Check函数检查当前节点的左子树和右子树,只有当左右子树都返回true时,当前节点才返回true。
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);
}
b. IsBalance 函数
IsBalance 函数用于检查整棵红黑树是否平衡。
-
根节点颜色检查:如果根节点存在且为红色,则直接返回
false,因为根节点必须是黑色(性质 2)。 -
计算最左侧路径黑色节点数:从根节点开始,沿着最左侧路径遍历树,计算遇到的黑色节点数,存储在
refBlackNum中。 -
调用
Check函数:从根节点开始,调用Check函数递归地检查整棵树是否满足红黑树的性质。传递的初始黑色节点数blackNum为 0,参考黑色节点数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);
}
如果 Check 函数返回 true,则说明红黑树平衡,IsBalance 函数也返回 true;否则,返回 false。
本文代码置于:RBtree · 比奇堡的Zyb/每日学习 - 码云 - 开源中国 (gitee.com)
数据结构之红黑树插入与删除全程演示
梦醒潇湘 love 2013-01-23 11:15:08
分类: C/C++
引言
目前国内图书市场上,抑或网上讲解红黑树的资料层次不齐,混乱不清,没有一个完整而统一的阐述。而本人的红黑树系列四篇文章,虽然从头至尾,讲的有根有据,层次清晰,然而距离读者真正做到红黑树了然于胸,则还缺点什么。
而我们知道,即便在经典的算法导论一书上,也没有把所有的插入、删除情况一一道尽,直接导致了不少读者的迷惑,而红黑树系列第 4 篇文章:一步一图一代码,一定要让你真正彻底明白红黑树,虽然早已把所有的插入、删除情况都一一道尽了,但也缺了点东西。
缺点什么东西呢?对了,缺的就是一个完完整整的,包含所有插入、删除情况全部过程的全程演示图,即缺一个例子,缺一个完整的图来从头至尾阐述这一切。
ok,本文,即以 40 幅图来全程演示此红黑树的所有插入和删除情况。相信,一定会对您理解红黑树有所帮助。
话不絮烦,下面,本文便以此篇文章:一步一图一代码,一定要让你真正彻底明白红黑树为纲,从插入一个结点到最后插入全部结点,再到后来一个一个把结点全部删除的情况一一阐述。
为了有个完整统一,红黑树插入和删除情况在此合作成一篇文章。同时,由于本人的红黑树系列的四篇文章已经把红黑树的插入、删除情况都一一详尽的阐述过了,因此,有关红黑树的原理,本文不再赘述,只侧重于用图来一一全程演示结点的插入和删除情况。
有任何问题,欢迎指正。
一、红黑树的插入情况全程演示
通过红黑树系列的文章,我们已经知道,红黑树的所有插入情况分为以下五种:
情况 1:新结点 N 位于树的根结点,没有父结点
情况 2:新结点的父结点 P 是黑色
情况 3:父结点 P、叔叔结点 U,都为红色
【对应于第二篇文章中的情况 1:z 的叔叔是红色的】
情况 4:父结点 P 是红色的,叔叔结点 U 是黑色或 NULL
【对应第二篇文章中的情况 2:z 的叔叔是黑色的,且 z 是右孩子】
情况 5:父结点 P 是红色,而叔叔结点 U 是黑色或 NULL
要插入的结点 N 是其父结点的左孩子,而父结点 P 又是其祖父 G 的左孩子
【对应第二篇文章中情况 3:z 的叔叔是黑色的,且 z 是左孩子】
首先,各个结点插入与以上的各种插入情况,一一对应起来,如下图所示。

以下是 20 张图,是依次插入这些结点:12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 的全程演示图,已经把所有的 5 种插入情况,都涉及到了。
第一图:插入结点 12

第二图:插入结点 1

第三图:插入结点 9

第四图:插入结点 2

第五图:插入结点 0

第六图:插入结点 11

第七图:插入结点 7

第八图:插入结点 19

第九图:插入结点 4

第十图:插入结点 15

第十一图:插入结点 18

第十二图:插入结点 5

第十三图:插入结点 14

第十四图:插入结点 13

第十五图:插入结点 10

第十六图:插入结点 16

第十七图:插入结点 6

第十八图:插入结点 3

第十九图:插入结点 8

第二十图:插入结点 17

二、红黑树的删除情况全程演示
红黑图的所有删除情况,如下所示。
情况 1:N 是新的根
情况 2:兄弟结点 S 是红色的
【对应于第二篇文章中情况 1:x 的兄弟结点 w 是红色的】
情况 3:兄弟结点 S 是黑色的,却 S 的两个儿子都是黑色的。但 N 的父结点 P 是黑色
【对应于第二篇文章中情况 2:x 的兄弟结点 w 是黑色的,且兄弟结点 w 的两个儿子都是黑色的】
(这里,N 的父结点 P 为黑色)
情况 4:兄弟结点 S 是黑色的,S 的儿子也都是黑色的,但是 N 的父亲 P 是红色
【对应于第二篇文章中情况 2:x 的兄弟结点 w 是黑色的,且 w 的两个孩子结点都是黑色的】
(这里,N 的父结点 P 为红色)
情况 5:兄弟结点 S 为黑色,S 的左孩子为红色,S 的右孩子是黑色,而 N 是它父结点的左儿子
// 此种情况,最后转化为下面的情况 6
【对应于第二篇文章中情况 3:x 的兄弟 w 是黑色的,w 的左孩子是红色的,w 的右孩子是黑色】
情况 6:兄弟结点 S 是黑色的,S 的右孩子是红色的,而 N 是它父结点的左儿子
【对应于第二篇文章中情况 4:x 的兄弟结点 w 是黑色的,且 w 的右孩子是红色的】
各个结点的删除与以上六种情况一一对应起来,如下图所示。

首先,插入上述结点后,形成的红黑树为:

然后,以下的 20 张图,是一一删除 12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 所得到的删除情况的全程演示图。
第一图:删除结点 12

第二图:删除结点 1

第三图:删除结点 9

第四图:删除结点 2

第五图:删除结点 0

第六图:删除结点 11

第七图:删除结点 7

第八图:删除结点 19

第九图:删除结点 4

第十图:删除结点 15

第十一图:删除结点 18

第十二图:删除结点 5

第十三图:删除结点 14

第十四图:删除结点 13

第十五图:删除结点 10

第十六图:删除结点 16

第十七图:删除结点 6

第十八图:删除结点 3

第十九图:删除结点 8

第二十图:删除结点 17

via:
-
Red-Black Tree / 红黑树. 树的搜寻 | by AWN, Feb 7, 2020
https://medium.com/@imprld01/red-black-tree-紅黑樹-8d793e692d70 -
最容易懂得红黑树-优快云博客 Sun_TTTT 于 2017-03-23 17:00:58 发布
https://blog.youkuaiyun.com/Sun_TTTT/article/details/65445754 -
红黑树介绍-优快云博客 RWCC于 2022-05-28 10:30:00 发布
https://blog.youkuaiyun.com/mz474920631/article/details/124982316 -
【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树-优快云博客 小七mod已于 2023-11-04 14:49:41 修改
https://blog.youkuaiyun.com/cy973071263/article/details/122543826 -
深入解析红黑树(RB-Tree):原理、操作及应用-优快云博客 无敌岩雀已于 2024-08-31 20:05:41 修改
https://blog.youkuaiyun.com/weixin_73494835/article/details/136789058 -
探索AVL树:优雅的自平衡二叉搜索树_avl的特性-优快云博客 无敌岩雀 已于 2024-04-23 18:56:43 修改
https://blog.youkuaiyun.com/weixin_73494835/article/details/136660341 -
数据结构之红黑树插入与删除全程演示 梦醒潇湘 love 2013-01-23 11:15:08
http://blog.chinaunix.net/uid-26548237-id-3480169.html
1756

被折叠的 条评论
为什么被折叠?



