红黑树(Red Black Tree)
是一种自平衡的二叉查找树.除了符合二叉查找树的基本特性,它还有以下的特性:
[补充说明二叉查找树的特性:
特征一:
任意结点(包括根结点)的左子树上的结点的值都比这个结点得值小。
特征二:
任意结点(包括根结点)的右子树上的结点的值都比这个结点得值大。
特征三(前驱结点):
二叉查找树中序遍历完成后和这个节点相邻的前面的节点为该节点的前驱节点。
前驱结点的判断有3种情况,每种情况都有不同的判断方法:
(1)如果x结点存在左孩子,则"x的前驱结点"为 “x的左子树中结点值最大的结点”。
(2)如果x结点不存在左孩子,且x是它的父结点的右孩子,则"x的前驱结点"为 “x的父结点”。
(3)如果x结点不存在左孩子,且x是它的父结点的左孩子,从x顺着线往上延伸,一直延伸到根结点,如果延伸过程中延伸到的点为其父亲结点"y"的右孩子,那么这个父亲结点"y"就是x的前驱结点,如果延伸过程中没有符合条件的,则这个x没有前驱结点。
最简单的方法还是把树中序遍历一下,一目了然。
特征四(后继结点):
二叉查找树中序遍历完成后和这个节点相邻的后面的节点为该节点的后继节点。
后继结点的判断有3种情况(跟前驱相反),每种情况都有不同的判断方法:
(1)如果x结点存在右孩子,则"x的后继结点"为 “x的右子树中结点值最小的结点”。
(2)如果x结点不存在右孩子,且x是它的父结点的左孩子,则"x的后继结点"为 “x的父结点”。
(3)如果x结点不存在右孩子,且x是它的父结点的右孩子,从x顺着线往上延伸,一直延伸到根结点,如果延伸过程中延伸到的点为其父亲结点"y"的左孩子,那么这个父亲结点"y"就是x的后继结点,如果延伸过程中没有符合条件的,则这个x没有后继结点。
特征五:
没有值相等的点。
实现二叉查找树的插入,删除,查找,前驱后继等算法都要根据它的基本特征性质来实现。]
它还具有以下的附加特性:
1.节点是红色或者黑色;
2.根节点是黑色;
3.每个叶子节点都是黑色的空节点(NULL节点),指定每个叶子的节点都是空节点;
4.每个红色节点的两个子节点都是黑色.(从每个叶子到根的所有路径上不能有两个连续的红色节点);
5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点;
(后续的插入,删除操作都是为了遵守这一规则)
2019-07-16_105003.jpg
该图是为了我方便记忆画的,只是体现了2,3的特性,4,5的特性还得自己记忆.
红黑树从根到叶子的最长的路径不会超过最短的2倍.
当红黑树规则被打破后继续调整为符合红黑树的方法:
- 变色
为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色。
如图:
当插入21是如左图情况;需要注意节点25并非根节点。因为节点21和节点22连续出现了红色,不符合规则4,所以把节点22从红色变成黑色:
20190127224846773.png
因为凭空多出的黑色节点打破了规则5,所以发生连锁反应,需要继续把节点25从黑色变成红色:
20190127224853938.png
25变色后,则出现了两个连续红色,打破了规则4,则需要继续把27从红色变为黑色;
2019012722490152.png
- 旋转
- 左旋转:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子;
如图:
-
右旋转:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子;
如图:
20190127224929533.png
黑色高度:从根节点到叶子节点的路径上黑色节点的个数.
红黑树的应用:
JDK的集合类,TreeMap和TreeSet底层就是红黑树实现的,在JDK1.8中,连HashMap也用到了红黑树;
2019-07-16_111834.jpg
Java集合框架中的TreeMap中的代码的左右旋
- 指点节点y的左旋(右图变左图)
//这里的p代表y
private void rotateLeft(Entry<K,V> p) {
if (p != null) { //获取P的右子节点,其实这里就相当于新增节点N(情况四而言)
Entry<K,V> r = p.right; // p是y,则r是x 将r的左子树设置为p的右子树
p.right = r.left; //若r的左子树不为空,则将P设置为r左子树的父亲(即左旋后,y的右子树变成x的左子树c)
if (r.left != null)
r.left.parent = p; //c确认父亲,将P的父亲设置R的父亲
r.parent = p.parent; //x取代y的第一步,认y的父亲为爹
if (p.parent == null)//如果y没有父亲,那x就是最老的根节点
root = r; //如果P为其父节点(G)的左子树,则将R设置为P父节点(G)左子树
else if (p.parent.left == p) //如果y有父亲,并且是它父亲的左孩子,y的父亲现在认x为左孩子,不要y了
p.parent.left = r;
else //如果y是父亲的右孩子,父亲就认x为右孩子,抛弃y
p.parent.right = r;
r.left = p; //将R设置为P的父节点,x逆袭成功,以前的爸爸现在成为它的孩子
p.parent = r;
}
}
简记:左旋把右子树里的一个节点(上图c)移到了左子树
- 指定节点x的右旋(左图转右图)
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null)
l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;右子树
else if (p.parent.right == p)
p.parent.right = l;
else
p.parent.left = l;
l.right = p;
p.parent = l;
}
}
简记:右旋把左子树里的节点(上图c)移到了右子树.
下面两幅图可以更生动地体现:
222222425437080.gif 222222452156364.gif
关于红黑树的查找,插入和删除操作
红黑树查找
因为红黑树是一颗二叉平衡树,并且查找不会破坏树的平衡,所以查找跟二叉平衡树的查找无异:
-
从根结点开始查找,把根结点设置为当前结点;
-
若当前结点为空,返回null;
-
若当前结点不为空,用当前结点的key跟查找key作比较;
-
若当前结点key等于查找key,那么该key就是查找目标,返回当前结点;
-
若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤2;
-
若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤2;
156196237_6_20190311074251207.jpg
正由于红黑树总保持黑色完美平衡,所以它的查找最坏时间复杂度为O(2lgN),也即整颗树刚好红黑相隔的时候。
红黑树插入
插入操作包括两部分工作:一查找插入的位置;二插入后自平衡。查找插入的父结点很简单,跟查找操作区别不大:
- 从根结点开始查找;
- 若根结点为空,那么插入结点作为根结点,结束。
- 若根结点不为空,那么把根结点作为当前结点;
- 若当前结点为null,返回当前结点的父结点,结束。
- 若当前结点key等于查找key,那么该key所在结点就是插入结点,更新结点的值,结束。
- 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤4;
-
若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤4;
156196237_7_20190311074251379.jpg
红黑树插入位置查找ok,插入位置已经找到,把插入结点放到正确的位置就可以啦,但插入结点是应该是什么颜色呢?答案是<u>红色</u>。理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作。但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,必须做自平衡。
插入情景
除了情景2,所有插入操作都是在叶子结点进行的
156196237_9_20190311074251863.jpg
插入操作结点的叫法约定
字母并不代表结点Key的大小。I表示插入结点,P表示插入结点的父结点,S表示插入结点的叔叔结点,PP表示插入结点的祖父结点。
下面让我们一个一个来分析每个插入的情景以其处理。
- 插入情景1:红黑树为空树
最简单的一种情景,直接把插入结点作为根结点就行,但注意,根据红黑树性质2:根节点是黑色。还需要把插入结点设为黑色。
处理:把插入结点作为根结点,并把结点设置为黑色 - 插入情景2:插入结点的Key已存在
插入结点的Key已存在,既然红黑树总保持平衡,在插入前红黑树已经是平衡的,那么把插入结点设置为将要替代结点的颜色,再把结点的值更新就完成插入。
处理:把I设为当前结点的颜色
更新当前结点的值为插入结点的值 - 插入情景3:插入结点的父结点为黑结点
由于插入的结点是红色的,当插入结点的黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡。
处理:直接插入 - 插入情景4:插入结点的父结点为红结点
再次回想下红黑树的性质2:根结点是黑色
如果插入的父结点为红结点,那么该父结点不可能为根结点,所以插入结点总是存在祖父结点
这点很重要,因为后续的旋转操作肯定需要祖父结点的参与。 - 情景4又分为很多子情景,下面将进入重点部分,各位看官请留神了。
- 插入情景4.1:叔叔结点存在并且为红结点
从红黑树性质4可以,祖父结点肯定为黑结点,因为不可以同时存在两个相连的红结点。那么此时该插入子树的红黑层数的情况是:黑红红。显然最简单的处理方式是把其改为:红黑红。
-
处理:将P和S设置为黑色
将PP设置为红色
把PP设置为当前插入结点
![156196237_12_2019031107425251.jpg](https://img.haomeiwen.com/i1932485/cd41122b254f8c35.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看到,我们把PP结点设为红色了,如果PP的父结点是黑色,那么无需再做任何处理;但如果PP的父结点是红色,根据性质4,此时红黑树已不平衡了,所以还需要把PP当作新的插入结点,继续做插入操作自平衡处理,直到平衡为止。
试想下PP刚好为根结点时,那么根据性质2,我们必须把PP重新设为黑色,那么树的红黑结构变为:黑黑红。换句话说,从根结点到叶子结点的路径中,黑色结点增加了。这也是唯一一种会增加红黑树黑色结点层数的插入情景。我们还可以总结出另外一个经验:红黑树的生长是自底向上的。这点不同于普通的二叉查找树,普通的二叉查找树的生长是自顶向下的。
-
插入情景4.2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子结点
单纯从插入前来看,也即不算情景4.1自底向上处理时的情况,叔叔结点非红即为叶子结点(Nil)。因为如果叔叔结点为黑结点,而父结点为红结点,那么叔叔结点所在的子树的黑色结点就比父结点所在子树的多了,这不满足红黑树的性质5。后续情景同样如此,不再多做说明了。
前文说了,需要旋转操作时,肯定一边子树的结点多了或少了,需要租或借给另一边。插入显然是多的情况,那么把多的结点租给另一边子树就可以了。 -
插入情景4.2.1:插入结点是其父结点的左子结点
处理:
- 将P设为黑色
- 将PP设为红色
-
对PP进行右旋
156196237_12_2019031107425251.jpg
-
插入情景4.2.1
左边两个红结点,右边不存在,那么一边一个刚刚好,并且因为为红色,肯定不会破坏树的平衡。
咦,可以把PP设为红色,I和P设为黑色吗?答案是可以!看过《算法:第4版》的同学可能知道,书中讲解的就是把PP设为红色,I和P设为黑色。但把PP设为红色,显然又会出现情景4.1的情况,需要自底向上处理,做多了无谓的操作,既然能自己消化就不要麻烦祖辈们啦~ -
插入情景4.2.2:
插入结点是其父结点的右子结点
这种情景显然可以转换为情景4.2.1,
不做过多说明了。
处理:
- 对P进行左旋
- 把P设置为插入结点,得到情景4.2.1
-
进行情景4.2.1的处理
156196237_13_20190311074252191.jpg
- 插入情景4.3.1:插入结点是其父结点的右子结点
处理:
- 将P设为黑色
- 将PP设为红色
-
对PP进行左旋
156196237_14_20190311074252285.jpg
)
- 插入情景4.3.2:插入结点是其父结点的右子结点
处理:
- 对P进行右旋
- 把P设置为插入结点,得到情景4.3.1
-
进行情景4.3.1的处理
156196237_15_20190311074252332.jpg
红黑树删除
红黑树插入已经够复杂了,但删除更复杂,也是红黑树最复杂的操作了。
红黑树的删除操作也包括两部分工作:
一 查找目标结点;而删除后自平衡。查找目标结点显然可以复用查找操作,当不存在目标结点时,忽略本次操作;当存在目标结点时,删除后就得做自平衡处理了。
删除了结点后我们还需要找结点来替代删除结点的位置,不然子树跟父辈结点断开了,除非删除结点刚好没子结点,那么就不需要替代。二叉树删除结点找替代结点有3种情情景:
- 情景1:若删除结点无子结点,直接删除
- 情景2:若删除结点只有一个子结点,用子结点替换删除结点
- 情景3:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点
补充说明下,情景3的后继结点是大于删除结点的最小结点,也是删除结点的右子树种最右结点。那么可以拿前继结点(删除结点的左子树最左结点)替代吗?可以的。但习惯上大多都是拿后继结点来替代,后文的讲解也是用后继结点来替代。另外告诉大家一种找前继和后继结点的直观的方法(不知为何没人提过,大家都知道?):把二叉树所有结点投射在X轴上,所有结点都是从左到右排好序的,所有目标结点的前后结点就是对应前继和后继结点!
156196237_17_20190311074252676.jpg讲一个重要的思路:删除结点被替代后,在不考虑结点的键值的情况下,对于树来说,可以认为删除的是替代结点!话很苍白,我们看下图。在不看键值对的情况下,下图的红黑树最终结果是删除了Q所在位置的结点!这种思路非常重要,大大简化了后文讲解红黑树删除的情景!
基于此,上面所说的3种二叉树的删除情景可以相互转换并且最终都是转换为情景1!
情景2:删除结点用其唯一的子结点替换,子结点替换为删除结点后,可以认为删除的是子结点,若子结点又有两个子结点,那么相当于转换为情景3,一直自顶向下转换,总是能转换为情景1。(对于红黑树来说,根据性质5.1,只存在一个子结点的结点肯定在树末了)
情景3:删除结点用后继结点(肯定不存在左结点),如果后继结点有右子结点,那么相当于转换为情景2,否则转为为情景1。
二叉树删除结点情景关系图如下图所示。
156196237_19_20190311074252926.jpg
综上所述,删除操作删除的结点可以看作删除替代结点,而替代结点最后总是在树末。有了这结论,我们讨论的删除红黑树的情景就少了很多,因为我们只考虑删除树末结点的情景了
同样的,我们也是先来总体看下删除操作的所有情景,如下图所示。
156196237_20_2019031107425319.jpg
即使简化了还是有9种情景!但跟插入操作一样,存在左右对称的情景,只是方向变了,没有本质区别。同样的,我们还是来约定下,如下图所示
156196237_21_20190311074253269.jpg
字母并不代表结点Key的大小。R表示替代结点,P表示替代结点的父结点,S表示替代结点的兄弟结点,SL表示兄弟结点的左子结点,SR表示兄弟结点的右子结点。灰色结点表示它可以是红色也可以是黑色。
值得特别提醒的是,R是即将被替换到删除结点的位置的替代结点,在删除前,它还在原来所在位置参与树的子平衡,平衡后再替换到删除结点的位置,才算删除完成。
- 删除情景1:替换结点是红色结点
我们把替换结点换到了删除结点的位置时,由于替换结点时红色,删除也了不会影响红黑树的平衡,只要把替换结点的颜色设为删除的结点的颜色即可重新平衡。
处理:颜色变为删除结点的颜色 - 删除情景2:替换结点是黑结点
当替换结点是黑色时,我们就不得不进行自平衡处理了。我们必须还得考虑替换结点是其父结点的左子结点还是右子结点,来做不同的旋转操作,使树重新平衡。
- 删除情景2.1:替换结点是其父结点的左子结点
- 删除情景2.1.1:替换结点的兄弟结点是红结点
若兄弟结点是红结点,那么根据性质4,兄弟结点的父结点和子结点肯定为黑色,不会有其他子情景,我们按图21处理,得到删除情景2.1.2.3(后续讲解,这里先记住,此时R仍然是替代结点,它的新的兄弟结点SL和兄弟结点的子结点都是黑色)。
处理:
1.将S设为黑色
2.将P设为红色
3.对P进行左旋,得到情景2.1.2.3
4.进行情景2.1.2.3的处理
156196237_22_20190311074253598.jpg
删除情景2.1.2:替换结点的兄弟结点是黑结点
当兄弟结点为黑时,其父结点和子结点的具体颜色也无法确定(如果也不考虑自底向上的情况,子结点非红即为叶子结点Nil,Nil结点为黑结点),此时又得考虑多种子情景。
删除情景2.1.2.1:替换结点的兄弟结点的右子结点是红结点,左子结点任意颜色
即将删除的左子树的一个黑色结点,显然左子树的黑色结点少1了,然而右子树又又红色结点,那么我们直接向右子树“借”个红结点来补充黑结点就好啦,此时肯定需要用旋转处理了。如下图所示。
处理:
- 将S的颜色设为P的颜色
- 将P设为黑色
- 将SR设为黑色
-
对P进行左旋
156196237_23_20190311074253738.jpg
平衡后的图怎么不满足红黑树的性质?前文提醒过,R是即将替换的,它还参与树的自平衡,平衡后再替换到删除结点的位置,所以R最终可以看作是删除的。另外图2.1.2.1是考虑到第一次替换和自底向上处理的情况,如果只考虑第一次替换的情况,根据红黑树性质,SL肯定是红色或为Nil,所以最终结果树是平衡的。如果是自底向上处理的情况,同样,每棵子树都保持平衡状态,最终整棵树肯定是平衡的。后续的情景同理,不做过多说明了。
删除情景2.1.2.2:替换结点的兄弟结点的右子结点为黑结点,左子结点为红结点
兄弟结点所在的子树有红结点,我们总是可以向兄弟子树借个红结点过来,显然该情景可以转换为情景2.1.2.1。如下图所示。
处理:
- 将S设为红色
- 将SL设为黑色
- 对S进行右旋,得到情景2.1.2.1
-
进行情景2.1.2.1的处理
156196237_24_20190311074253801.jpg
删除情景2.1.2.3:替换结点的兄弟结点的子结点都为黑结点
好了,此次兄弟子树都没红结点“借”了,兄弟帮忙不了,找父母呗,这种情景我们把兄弟结点设为红色,再把父结点当作替代结点,自底向上处理,去找父结点的兄弟结点去“借”。但为什么需要把兄弟结点设为红色呢?显然是为了在P所在的子树中保证平衡(R即将删除,少了一个黑色结点,子树也需要少一个),后续的平衡工作交给父辈们考虑了,还是那句,当每棵子树都保持平衡时,最终整棵总是平衡的。
处理:
- 将S设为红色
- 把P作为新的替换结点
-
重新进行删除结点情景处理
156196237_25_20190311074253957.jpg
删除情景2.2:替换结点是其父结点的右子结点
好啦,右边的操作也是方向相反,不做过多说明了,相信理解了删除情景2.1后,肯定可以理解2.2。
删除情景2.2.1:替换结点的兄弟结点是红结点
处理:
- 将S设为黑色
- 将P设为红色
- 对P进行右旋,得到情景2.2.2.3
-
进行情景2.2.2.3的处理
156196237_26_20190311074254129.jpg
删除情景2.2.2:替换结点的兄弟结点是黑结点
删除情景2.2.2.1:替换结点的兄弟结点的左子结点是红结点,右子结点任意颜色
处理:
- 将S的颜色设为P的颜色
- 将P设为黑色
- 将SL设为黑色
-
对P进行右旋
156196237_27_20190311074254270.jpg
删除情景2.2.2.2:替换结点的兄弟结点的左子结点为黑结点,右子结点为红结点
处理:
1.将S设为红色
2.将SR设为黑色
3.对S进行左旋,得到情景2.2.2.1
4.进行情景2.2.2.1的处理
156196237_28_20190311074254410.jpg
删除情景2.2.2.3:替换结点的兄弟结点的子结点都为黑结点
处理:
- 将S设为红色
- 把P作为新的替换结点
-
重新进行删除结点情景处理
156196237_29_20190311074254566.jpg
综上,红黑树删除后自平衡的处理可以总结为:
自己能搞定的自消化(情景1)
自己不能搞定的叫兄弟帮忙(除了情景1、情景2.1.2.3和情景2.2.2.3)
兄弟都帮忙不了的,通过父母,找远方亲戚(情景2.1.2.3和情景2.2.2.3)
哈哈,是不是跟现实中很像,当我们有困难时,首先先自己解决,自己无力了总兄弟姐妹帮忙,如果连兄弟姐妹都帮不上,再去找远方的亲戚了。这里记忆应该会好记点~
最后关于红黑树的面试题
阿里面试题:为什么Map桶中个数超过8才转为红黑树?
因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。至于为什么阈值是8,我想,去源码中找寻答案应该是最可靠的途径。8这个阈值定义在HashMap中,如下所示,这段注释只说明了8是bin(bin就是bucket,即HashMap中hashCode值一样的元素保存的地方)从链表转成树的阈值,但是并没有说明为什么是8:
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
我们继续往下看,在HashMap中有一段Implementation notes,笔者摘录了几段重要的描述,第一段如下所示,大概含义是当bin变得很大的时候,就会被转换成TreeNodes中的bin,其结构和TreeMap相似,也就是红黑树
This map usually acts as a binned (bucketed) hash table, butwhen bins get too large, they are transformed into bins of TreeNodes,each structured similarly to those in java.util.TreeMap
继续往下看,TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。这样就解析了为什么不是一开始就将其转换为TreeNodes,而是需要一定节点数才转为TreeNodes,说白了就是trade-off,空间和时间的权衡:
Because TreeNodes are about twice the size of regular nodes, weuse them only when bins contain enough nodes to warrant use(see TREEIFY_THRESHOLD). And when they become too small (due toremoval or resizing) they are converted back to plain bins. Inusages with well-distributed user hashCodes, tree bins arerarely used. Ideally, under random hashCodes, the frequency ofnodes in bins follows a Poisson distribution(http://en.wikipedia.org/wiki/Poisson_distribution) with aparameter of about 0.5 on average for the default resizingthreshold of 0.75, although with a large variance because ofresizing granularity. Ignoring variance, the expectedoccurrences of list size k are (exp(-0.5)*pow(0.5, k)/factorial(k)). The first values are:0: 0.606530661: 0.303265332: 0.075816333: 0.012636064: 0.001579525: 0.000157956: 0.000013167: 0.000000948: 0.00000006more: less than 1 in ten million
这段内容还说到:当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。由此可见,发展30年的Java每一项改动和优化都是非常严谨和科学的。
这是个人根据一些文档整理的,非原创,个人觉得比较容易理解,所以整理了
网友评论