二叉树:二叉树是每一个结点最多有两个子树的树结构;一般子树被称做“左子树”(left subtree)和“右子树”(right subtree);二叉树常被用于实现二叉查找树和二叉堆;html
平衡二叉树:又被称为AVL树(有别于AVL算法),且具备如下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,而且左右两个子树都是一棵平衡二叉树。平衡二叉树的经常使用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等;java
排序二叉树:任何节点的键值必定大于其左子树中的每个节点的键值,并小于其右子树中的每个节点的键值;算法
红黑树: spa
红黑树在原有的排序二叉树增长了以下几个要求:code
性质 1:每一个节点要么是红色,要么是黑色。
性质 2:根节点永远是黑色的。
性质 3:全部的叶节点都是空节点(即 null,其实是不存在的节点),而且是黑色的。
性质 4:每一个红色节点的两个子节点都是黑色。(从每一个叶子到根的路径上不会有两个连续的红色节点)
性质 5:从任一节点到其子树中每一个叶子节点的路径都包含相同数量的黑色节点。htm
红黑树除了具备排序二叉树特性也属于平衡二叉树,但不是严格的平衡二叉树,说它不严格是由于它不是严格控制左、右子树高度或节点数之差小于等于1,但红黑树高度依然是平均log(n),且最坏状况高度不会超过2log(n),这有数学证实。因此它算平衡树。blog
在树的结构发生改变时(插入或者删除操做),每每会破坏上述条件4或条件5,须要经过调整使得查找树从新知足红黑树的条件;须要经过调整使得查找树从新知足红黑树的条件。排序
调整能够分为两类:一类是颜色调整,即改变某个节点的颜色;另外一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操做:左旋(RotateLeft),右旋(RotateRight)。递归
左旋的过程是将x
的右子树绕x
逆时针旋转,使得x
的右子树成为x
的父亲(x成为其右子树的左节点),同时修改相关节点的引用。旋转以后,二叉查找树的属性仍然知足。get
以下,3--->9--->10这个链即是以3节点为当前节点,3节点的右子树9--->10逆时针左旋的效果,最后3成了9的左节点,三、九、10达成了平衡
右旋的过程是将x
的左子树绕x
顺时针旋转,使得x
的左子树成为x
的父亲(x成为其左子树的右节点),同时修改相关节点的引用。旋转以后,二叉查找树的属性仍然知足。
以下,3--->2--->1这个链即是以3节点为当前节点,3节点的左子树2--->1顺时针时针右旋的效果,最后3成了2的右节点,三、二、1达成了平衡;
增长节点后有哪些场景是须要调整的?
在一个已有的红黑树中增长一个新节点,假设3为祖父节点,2或4为父(叔)节点,新加入的节点为当前节点,当前新增节点做为2或4的子节点;能够用排除法来一个个排除,场景以下:
综合分析,一颗存在红黑树自己处于相对稳定状态(没有外力能触发调整),稳定的红黑树中只会存在上述无影响和待调整的结构图,不会出现不存在的结构图;而无影响的结构图新增节点并不破坏红黑树特性,因此待处理的就剩下待调整的三种了,接下来分析这三种。
注意,看图时注意结构,不要盯着纯数字,由于不一样场景,数字表明的节点含义不同
场景一:祖父节点黑色、父节点为祖父节点的左节点、无叔叔节点
这种状况,若是是孙节点做为父节点的右节点加入,对应1.1开始;若是孙节点做为父节点的左节点加入,对应1.2开始;
场景二:祖父节点黑色、父节点为祖父节点的右节点、无叔叔节点
这种状况,若是是孙节点做为父节点的左节点加入,对应2.1开始;若是孙节点做为父节点的右节点加入,对应2.2开始;
场景三:祖父节点黑色、父节点红色、叔叔节点红色
这种状况,若是不管孙节点是做为父节点的左节点仍是右节点,或者不管父节点是哪个红色节点,处理方式都是统一的
这种场景下,最后一步,祖父节点变成了红色,而祖父节点的父节点可能以前也是红色的,因此可能违背了红黑树性质,因此才会有最后一步将祖父节点设为“当前节点”,而后就成为了场景一和二的状况,这是一个递归的过程直到当前节点的父节点颜色是黑色。
参考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
参考: