上一篇:
详解平衡二叉搜索树( AVL树 二叉树) 操作(附图解)Java实现
红黑树
目录
(3)、每个叶节点(叶节点即指树尾端NIL或NULL节点)是黑的;
(5)、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
1、二叉查找树介绍
在某些极端情况下,如果值都是比根节点大,都在右侧,
根的右孙子节点比右儿子节点大,都在树的右边,那么查询效率就低了;
所以为了平衡二叉查找树,我们发明了AVL平衡树,让二叉树的深度比较平均,
让每一个节点的左边和右边的数量都差不多,高度相差不超过1;
2、红黑树来源
红黑树和AVL平衡树,都属于二叉搜索树;
为什么有了AVL树,还需要发明红黑树;
在插入或者删除数据到时候,为了使树变得平衡,有一种树的操作,叫做旋转;
对于AVL树在这里不再多描述:
可以参考如下帖子:https://blog.youkuaiyun.com/lejustdoit/article/details/93634095
如果右边超出了高度,那么需要左旋;相反则需要右旋;
AVL树到了后面,为了保持树的平衡,需要进行的旋转操作次数比较多,比较耗时;
所以,在选择数据结构的时候,如果查询比较多,我们选择AVL树没有问题;
但是在插入删除情况比较多的时候,红黑树的效率就要比AVL树高了,并且红黑树的查找效率也基本和AVL树差不多;这就是红黑树的来源 ;
3、红黑树特性
(1)、每个节点都只能是红色或者黑色;
(2)、根节点是黑色;
(3)、每个叶节点(叶节点即指树尾端NIL或NULL节点)是黑的;
(4)、如果一个结点是红的,则它两个子节点都是黑的。
也就是说在一条路径,上不能出现相邻的两个红色结点。一般用黑的NIL节点表示叶节点,不包含值,
只是标志该分支结束,有时候绘图中会直接省略;
(5)、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
从上述特性,我们可以得到以下隐含的特性:
如果一个节点的颜色是红色的,那么他的子节点必须是黑色的;
从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;
假如高度为9,那么到子节点有5个黑色节点,那么就有4个红色节点;
假设另外一个分支至少应该也有5个黑色节点,如果是全黑节点的话,那么5和9的数量差不会超过两倍,也就是说,树的高度差不会超过两倍;
正是这些性质的限制,使得红黑树中任一节点到其子孙叶子节点的最长路径不会长于最短路径的2倍,因此它是一种接近平衡的二叉树。
在执行插入、删除操作时,AVL树需要调整的次数一般要比红黑树多(红黑树的旋转调整最多只需三次),
效率相对较低,且红黑树的统计性能较AVL树要好,
当然AVL树在查询效率上可能更胜一筹,但实际上也高不了多少;所以综合来讲,红黑树的综合性能效率好一些;
在JDK中TreeMap和1.8版本以后的HashMap里面都有用到红黑树的结构;
4、红黑树插入
插入尽可能不破坏红黑树的性质,根据上述中红黑树特性(5),不能将新插入的节点涂成黑色,因为这样所有经过新插入点的路径的黑色节点都多了一个,就破坏了红黑树特性(5),少违背一条特性,就意味着我们需要处理的情况越少,所以我们将新插入的节点颜色首先着色为红;
为了方便以下操作,我们定义下述节点的关系,S(Son)代表子节点,F(Father)代表父节点,U(Uncle)代表叔叔节点,R(Root)代表子树根节点;插入的时候共有以下几种情况:
(1)、如果插入节点的父节点是黑色,那么没有违反任何红黑树性质,直接插入,无需进行旋转或者变色操作;
(2)、如果插入节点的父节点是红色,那么违反了红黑树特性(4),所以需要进行调整,这里共有四种情况:
a、新插入节点S的父节点F和其叔叔节点U(祖父节点R的另一个子节点)均为红色的,并且新插入节点比F父节点小,往左边插入;
此时,肯定存在祖父节点(因为根节点不可能为红色),对于这种情况,我们要做的操作有:将当前节点的父节点和叔叔节点涂黑,
将祖父节点涂红,再次从新的当前节点开始算法;将R节点当做S节点继续递归处理;
这个操作实际是想将红色往根处移动。将红色往上移了一层,并不会打破红黑树的特性,不断的把红色往上移动,当移动到根时,直接将根设置为黑色,就完全符合红黑树的性质了;
为什么需要这样处理?
“当前节点”和“父节点”都是红色,违背“特性(4)”。所以,将“父节点”设置“黑色”以解决这个问题。
但是,将“父节点”由“红色”变成“黑色”之后,违背了“特性(5)”:因为,包含“父节点”的分支的黑色节点的总数增加了1。 解决这个问题的办法是:将“祖父节点”由“黑色”变成红色,同时,将“叔叔节点”由“红色”变成“黑色”。关于这里,说明几点:第一,为什么“祖父节点”之前是黑色?这个应该很容易想明白,因为在变换操作之前,该树是红黑树,“父节点”是红色,那么“祖父节点”一定是黑色。 第二,为什么将“祖父节点”由“黑色”变成红色,同时,将“叔叔节点”由“红色”变成“黑色”;能解决“包含‘父节点’的分支的黑色节点的总数增加了1”的问题。这个道理也很简单。“包含‘父节点’的分支的黑色节点的总数增加了1” 同时也意味着 “包含‘祖父节点’的分支的黑色节点的总数增加了1”,既然这样,我们通过将“祖父节点”由“黑色”变成“红色”以解决“包含‘祖父节点’的分支的黑色节点的总数增加了1”的问题; 但是,这样处理之后又会引起另一个问题“包含‘叔叔’节点的分支的黑色节点的总数减少了1”,现在我们已知“叔叔节点”是“红色”,将“叔叔节点”设为“黑色”就能解决这个问题。 所以,将“祖父节点”由“黑色”变成红色,同时,将“叔叔节点”由“红色”变成“黑色”;就解决了该问题。
按照上面的步骤处理之后:当前节点、父节点、叔叔节点之间都不会违背红黑树特性,但祖父节点却不一定。若此时,祖父节点是根节点,直接将祖父节点设为“黑色”,那就完全解决这个问题了;若祖父节点不是根节点,那我们需要将“祖父节点”设为“新的当前节点”,接着对“新的当前节点”进行分析。
b、新插入节点S的父节点F和其叔叔节点U(祖父节点R的另一个子节点)均为红色的,并且新插入节点S比父节点F大,往右边插入;先以F为支点,进行子树左旋转;此时得到情况a的插入情形,那么把F再看做新插入的子节点,再进行重新递归着色;
c、新插入节点S的父节点F为红色,其叔叔节点U为黑色,并且新插入节点比F父节点小,左侧插入;将父节点F设为黑色,祖父节点R设为红色,然后再以F为支点,进行子树右旋转:
下面谈谈为什么要这样处理。(建议理解的时候,通过图进行对比)
为了便于说明,我们设置“当前节点”为S(Son),“兄弟节点”为B(Brother),“叔叔节点”为U(Uncle),“父节点”为F(Father),祖父节点为R;
S和F都是红色,违背了红黑树的“特性(4)”,我们可以将F由“红色”变为“黑色”,就解决了“违背‘特性(4)’”的问题;但却引起了其它问题:违背特性(5),因为将F由红色改为黑色之后,所有经过F的分支的黑色节点的个数增加了1。那我们如何解决“所有经过F的分支的黑色节点的个数增加了1”的问题呢? 我们可以通过“将R由黑色变成红色”,同时“以R为支点进行右旋”来解决。
d、当前节点S的父节点F是红色,叔叔节点U是黑色,且当前节点比其父节点大,准备往右侧插入;
将“父节点”F作为“新的当前节点”,以“新的当前节点”F为支点进行左旋。
这样又把F看做是新插入的节点,回到了情况c的插入情形;然后再进行重新着色,最后右旋转的操作
下面谈谈为什么要这样处理。(建议理解的时候,通过下面的图进行对比)
首先,将“父节点”作为“新的当前节点”;接着,以“新的当前节点”为支点进行左旋。 为了便于理解,我们先说明第一步“以F为支点进行左旋”;为了便于说明,我们设置“父节点”的代号为F(Father),“当前节点”的代号为S(Son)。
为什么要“以F为支点进行左旋”呢?根据已知条件可知:S是F的右孩子。而之前我们说过,我们处理红黑树的核心思想:将红色的节点移到根节点;然后,将根节点设为黑色。既然是“将红色的节点移到根节点”,那就是说要不断的将破坏红黑树特性的红色节点上移(即向根方向移动)。 而S又是一个右孩子,因此,我们可以通过“左旋”来将S上移!
按照上面的步骤(以F为支点进行左旋)处理之后:若S变成了根节点,那么直接将其设为“黑色”,就完全解决问题了;若S不是根节点,那我们需要执行步骤“将F设为‘新的当前节点’”。那为什么不继续以S为新的当前节点继续处理,而需要以F为新的当前节点来进行处理呢?这是因为“左旋”之后,F变成了S的“子节点”,即S变成了F的父节点;而我们处理问题的时候,需要从下至上(由叶到根)方向进行处理;也就是说,必须先解决“孩子”的问题,再解决“父亲”的问题;所以,我们执行步骤:将“父节点”作为“新的当前节点”。
5、红黑树的删除
将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗普通二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树;恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。 虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次
(这也就是为什么我们需要用红黑树的原因,更少的旋转高效,任何不平衡都会在3次旋转之内解决。这一点是AVL树不具备的);
删除后调整平衡思想:
为了保证删除节点父亲节点左右两边黑色节点数一致,需要重点关注父亲节点没删除的那一边节点是不是黑色。如果删除后父亲节点另一边比删除的一边黑色节点多,就要想办法平衡,具体的平衡方法有如下两种方法:
把父亲节点另一边(即删除节点的兄弟树)其中一个节点变红,这样少一个黑色;
或者把另一边多的黑色节点转过来一个;
(1)被删除的节点没有孩子节点,即叶子节点。
a、被删结点无子结点,且被删结点为红色,可以直接删除;
b、被删结点无子结点,且被删结点为黑色:
参考帖子:https://segmentfault.com/a/1190000012115424
(2)被删除的节点只有一个孩子节点
那么直接删除该节点,然后用它的孩子节点顶替它的位置。
(3)被删除的节点有两个孩子节点。
这种情况二叉树的删除有一个技巧,就是查找到要删除的节点X,接着我们找到它右子树的最左侧(最小)元素M,交换X和M的值,然后删除节点M。此时M就最多只有一个子节点N(右子树最小节点,已经最左,肯定没有左子节点 ),若M没有子节点,则进入(1)的情况,否则进入(2)的情况。
说明:为了表示红黑树的无关颜色(不关心红色或者黑色),用蓝色节点表示:
a、跟二叉树一样,我们假定节点X是要删除的节点,而节点M是找到X右子树的最小元素,所以节点M是X的替代节点,也就是说M是真正要删除的节点。此时的M只会有一个右子节点N(因为M已经是右子树的最小值了),当删除节点M后,N将替代M作为M节点的父节点的子节点。删除的节点M是黑色(删除红色就不影响),此时如果N是红色,只需将N设置为黑色,就会重新达到平衡,不会出现该路径上少了一个黑色节点的情况;但是如果N是黑色,情况则比较复杂,需要对红黑树进行调整,而这种情况又分为了以下几种,下面进行图解:
(a)、N的父节点P为红色,兄弟节点B和它的两个孩子节点也都是黑色。此时只需要交换P和B的颜色,将P改为黑色,B改为红色,则可到达平衡。这相当于既然节点N路径少了一个黑色节点,那么B路径也少一个黑色节点,这两个路径达到平衡,为了防止P路径少一个黑色节点,将P节点置黑,则达到最终平衡。
(b)、N的兄弟节点B是黑色,B的右孩子节点BR是红色,B的左孩子节点BL任意颜色,P任意颜色。方法是:BR变为黑色,P变为黑色,B变为P的颜色;
再左旋节点B。首先给N路径增加一个黑色节点P,P原位置上的颜色不变;B路径少了一个黑色节点,于是将BR改为黑色,最终达到了平衡。
(c)、N的兄弟节点B是红色(B为红色,P、BL、BR肯定是黑色)。方法是:交换P和B的颜色,左旋父节点P。此时并未完成平衡,左子树仍然少了一个黑色节点,把N再作为当前节点,进入情况(a)
(d)、N的父节点P是黑色,且兄弟节点B和它的两个孩子节点也都是黑色。方法是:将N的兄弟节点B改为红色,这样从P出发到叶子节点的路径都包含了相同的黑色节点,但是,对于节点P这个子树,P的父节点G到P的叶子节点路径上的黑色节点就少了一个,此时需要将P整体看做一个节点(把他看做其他几种情况的N节点,继续递归平衡操作),继续调整。
(e)、N的兄弟节点B是黑色,B的左孩子节点BL是红色,B的右孩子节点BR是黑色,P为任意颜色。方法是:交换B和BL的颜色,右旋节点B。此时N子树路径并没有增加黑色节点,也就是没有达到平衡,此时进入之前的情况(b):
参考帖子:https://www.jianshu.com/p/fc5e16b5c674
https://www.cnblogs.com/skywang12345/p/3245399.html
下一篇: