预备知识
二叉树:二叉树是每个结点最多有两个子树的树结构;通常子树被称作“左子树”(left subtree)和“右子树”(right subtree);二叉树常被用于实现二叉查找树和二叉堆;
平衡二叉树:又被称为AVL树(有别于AVL算法),且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等;
排序二叉树:任何节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值;
红黑树:
红黑树在原有的排序二叉树增加了如下几个要求:
性质 1:每个节点要么是红色,要么是黑色。
性质 2:根节点永远是黑色的。
性质 3:所有的叶节点都是空节点(即 null,实际上是不存在的节点),并且是黑色的。
性质 4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
性质 5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
红黑树除了具有排序二叉树特性也属于平衡二叉树,但不是严格的平衡二叉树,说它不严格是因为它不是严格控制左、右子树高度或节点数之差小于等于1,但红黑树高度依然是平均log(n),且最坏情况高度不会超过2log(n),这有数学证明。所以它算平衡树。
红黑树的自我调整
在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件4或条件5,需要通过调整使得查找树重新满足红黑树的条件;需要通过调整使得查找树重新满足红黑树的条件。
调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操作:左旋(RotateLeft),右旋(RotateRight)。
左旋
左旋的过程是将x
的右子树绕x
逆时针旋转,使得x
的右子树成为x
的父亲(x成为其右子树的左节点),同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
如下,3--->9--->10这个链便是以3节点为当前节点,3节点的右子树9--->10逆时针左旋的效果,最后3成了9的左节点,3、9、10达成了平衡
右旋
右旋的过程是将x
的左子树绕x
顺时针旋转,使得x
的左子树成为x
的父亲(x成为其左子树的右节点),同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
如下,3--->2--->1这个链便是以3节点为当前节点,3节点的左子树2--->1顺时针时针右旋的效果,最后3成了2的右节点,3、2、1达成了平衡;
增加节点场景分析
增加节点后有哪些场景是需要调整的?
在一个已有的红黑树中增加一个新节点,假设3为祖父节点,2或4为父(叔)节点,新加入的节点为当前节点,当前新增节点作为2或4的子节点;可以用排除法来一个个排除,场景如下:
- 如果3是红节点,那么只可能出现一种情况,2、4同时存在且都是黑色节点(3不可能有红色子节点,黑色子节点不能只出现一个),2和4都为黑色节点的情况新加入的节点无论是2还是4的子节点都不影响原有红黑树的性质,不需要调整,排除
- 再看,如果3节点为黑色,2或者4节点为也为黑色,那么2、4节点必须同时存在,不可能存在3只有一个黑色子节点的情况(违背了性质5);而3、2、4都为黑色节点的情况新加入的节点无论是2还是4的子节点都不影响原有红黑树的性质,不需要调整,排除
- 其实现在只剩下3是黑色节点,2或4均为红色节点,这种有分为只有2节点、只有4节点、2和4同时存在,其实反过来思考,正是因为有这三种情况的存在,每当新加入一个红色节点后会引起红黑树的自我调整,调整结束后就不会有能引起调整的条件了。
综合分析,一颗存在红黑树本身处于相对稳定状态(没有外力能触发调整),稳定的红黑树中只会存在上述无影响和待调整的结构图,不会出现不存在的结构图;而无影响的结构图新增节点并不破坏红黑树特性,所以待处理的就剩下待调整的三种了,接下来分析这三种。
注意,看图时注意结构,不要盯着纯数字,因为不同场景,数字代表的节点含义不一样
场景一:祖父节点黑色、父节点为祖父节点的左节点、无叔叔节点
这种情况,如果是孙节点作为父节点的右节点加入,对应1.1开始;如果孙节点作为父节点的左节点加入,对应1.2开始;
- 当前节点:2
- 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子
- 将“父节点”作为“新的当前节点”
- 以“新的当前节点”为支点进行左旋
- ----------------------------------分割线,如果从图1.2开始,只有以下步骤------------------------------------
- 当前节点:1
- 当前节点的父节点是红色,叔叔节点是黑色(不存在就是黑色,红黑色性质三),且当前节点是其父节点的左孩子
- 将“父节点”设为“黑色”
- 将“祖父节点”设为“红色”
- 以“祖父节点”为支点进行右旋
场景二:祖父节点黑色、父节点为祖父节点的右节点、无叔叔节点
这种情况,如果是孙节点作为父节点的左节点加入,对应2.1开始;如果孙节点作为父节点的右节点加入,对应2.2开始;
- 当前节点:4
- 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子
- 将“父节点”作为“新的当前节点”
- 以“新的当前节点”为支点进行右旋
- -------------------------------------分割线,如果从图2.2开始,只有以下步骤-------------------------------------------
- 当前节点:5
- 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子
- 将“父节点”设为“黑色”
- 将“祖父节点”设为“红色”
- 以“祖父节点”为支点进行左旋
场景三:祖父节点黑色、父节点红色、叔叔节点红色
这种情况,如果无论孙节点是作为父节点的左节点还是右节点,或者无论父节点是哪一个红色节点,处理方式都是统一的
- 当前节点:1
- 当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色
- 将“父节点”设为黑色
- 将“叔叔节点”设为黑色
- 将“祖父节点”设为“红色
- 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作
这种场景下,最后一步,祖父节点变成了红色,而祖父节点的父节点可能之前也是红色的,所以可能违背了红黑树性质,所以才会有最后一步将祖父节点设为“当前节点”,然后就成为了场景一和二的情况,这是一个递归的过程直到当前节点的父节点颜色是黑色。
参考java中TreeMap的实现,来看是否和上述分析一致
fixAfterInsertion是每次向TreeMap中新增节点后都会调用的修正方法,正式这个方法保证和红黑树的性质,与之对应的有fixAfterDeletion(删除节点后调用)
看这个方法的逻辑,完全与上述分析一致。
1 private void fixAfterInsertion(Entry<K,V> x) { 2 // 新增节点都是红色 3 x.color = RED; 4 5 // 递归处理,当前节点的父节点是红色就要一直处理 6 while (x != null && x != root && x.parent.color == RED) { 7 // 父节点为祖父节点的左节点 8 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { 9 Entry<K,V> y = rightOf(parentOf(parentOf(x))); 10 if (colorOf(y) == RED) { 11 // 叔叔节点为红色,父、叔节点置黑,祖父节点置红,以祖父节点为当前节点递归 12 setColor(parentOf(x), BLACK); 13 setColor(y, BLACK); 14 setColor(parentOf(parentOf(x)), RED); 15 x = parentOf(parentOf(x)); 16 } else { 17 if (x == rightOf(parentOf(x))) { 18 // 父节点左旋为祖父节点的右旋腾出位置(孙父祖三代处于同一斜率,依次为左子节点) 19 x = parentOf(x); 20 rotateLeft(x); 21 } 22 // 父节点置黑、祖父节点置红、祖父节点右旋 23 setColor(parentOf(x), BLACK); 24 setColor(parentOf(parentOf(x)), RED); 25 rotateRight(parentOf(parentOf(x))); 26 } 27 } 28 // 父节点为祖父节点的右节点 29 else { 30 Entry<K,V> y = leftOf(parentOf(parentOf(x))); 31 if (colorOf(y) == RED) { 32 // 叔叔节点为红色,父、叔节点置黑,祖父节点置红,以祖父节点为当前节点递归 33 setColor(parentOf(x), BLACK); 34 setColor(y, BLACK); 35 setColor(parentOf(parentOf(x)), RED); 36 x = parentOf(parentOf(x)); 37 } else { 38 if (x == leftOf(parentOf(x))) { 39 // 父节点右旋为祖父节点的左旋腾出位置(孙父祖三代处于同一斜率,依次为右子节点) 40 x = parentOf(x); 41 rotateRight(x); 42 } 43 // 父节点置黑、祖父节点置红、祖父节点左旋 44 setColor(parentOf(x), BLACK); 45 setColor(parentOf(parentOf(x)), RED); 46 rotateLeft(parentOf(parentOf(x))); 47 } 48 } 49 } 50 51 // 根节点置黑 52 root.color = BLACK; 53 }
思考
为甚麽红黑树中新增加的节点一定是红色?
红黑树的5个性质中,性质4和性质5是比较容易违背的,为了尽量避免因为破坏红黑树的特性而做调整,每次新插入的节点都是红色。因为插入之前所有根至外部节点的路径上黑色节点数目都相同,如果插入的节点是黑色肯定错误(黑色节点数目不相同),而相对的插入红节点可能会也可能不会违反“没有连续两个节点是红色”这一条件,所以插入的节点为红色代价相对小,如果违反条件再调整。
为什么基于“子节点-->父节点-->祖父节点”来调整红黑树的平衡?
在进行颜色变化或旋转的时候,往往要涉及祖孙三代节点(X表示操作的基准节点,P代表X的父节点,G代表X的父节点的父节点);这是因为基于至少三个节点来旋转调色可以尽量保持局部满足红黑树的5个特性,这样就能尽量不破坏整体特性;如果只有两个节点子和父,就算知道破坏了红黑树的性质也没法通过自我调整来达到效果,只有两个节点旋转来旋转去也不平衡。
为什么场景一和场景二中,孙节点作为父节点的左节点和右节点处理场景不一样?
拿场景一来说,如果不将下图中的1和2节点进行一次左旋,那么3节点在右旋的时候2会成为3的左节点,不能同时保证性质4和5;假设红黑树只有三个节点,右旋之后1成为根节点,3是1的右节点,2是3的左节点;由于1是黑色且3只能为红色,那么2节点为黑色破坏了性质5,2节点为红色破坏了性质4;
有人问为什么不把3往左旋,其实仔细想想红黑树的默认排序规则,一个节点的值一定大于其左子树小于其右子树。因此,保证孙--->父--->祖三代节点都在同一斜率(依次为左子节点或依次为右子节点)可以同时满足红黑树的五种性质并且尽可能保证红黑树的平衡。
删除节点场景分析
待补充
PS:在线生成红黑树连接:https://sandbox.runjs.cn/show/2nngvn8w
参考: