红黑树属于特殊的二叉搜索树,通过对任一条从根结点到叶子结点简单路径上的各个结点进行颜色约束,确保了没有一条路径会比其他路径长出两倍,是*乎*衡的。
- 有以下性质:
- 每个结点或是红色,或是黑色;
- 根结点是黑色的;
- 每个叶结点是黑色的;
- 如果一个结点是红色的,则其两个子结点都是黑色的;
- 对于每个结点x,从该结点到其所有后代叶结点的简单路径上,均包含了相同数量的黑色结点(即 黑高,记为 bh(x) 相同 )。
红黑树大量应用在底层数据结构中,主要用于存储和查找。*日开发虽然很少自己去写红黑树,但是深入了解数据结构和算法是非常必要的。
那红黑树都有哪些操作都是如何实现的……请看下文。
树中每个结点均包含5个属性:color、key、left、right、p
红黑树的插入、删除需要不断的维护上述5个性质……如何实现?两种操作:旋转(左旋、右旋)、变色(在插入、删除中实现);
旋转操作
1、左旋:
1 LEFT-ROTATE(T, x) 2 y = x.right 3 x.right = y.left 4 if y.left != T.nil then 5 y.left.p = x 6 y.p = x.p 7 if x.p == T.nil then 8 T.root = y 9 else if x == x.p.left then 10 x.p.left = y 11 else 12 x.p.right = y 13 y.left = x 14 x.p = y
2、右旋:
1 RIGHT-ROTATE(T, y) 2 x = y.left 3 y.left = x.right 4 if x.right != T.nil then 5 x.right.p = y 6 x.p = y.p 7 if y.p == T.nil then 8 T.root = x 9 else if y == y.p.left then 10 y.p.left = x 11 else
y.p.right = x 12 x.right = y 13 y.p = x
插入操作
与普通二叉搜索树差异不大,唯二的区别是:需要维护红黑树的5个性质,插入的结点的初始颜色是红色的(如果插入结点是黑色的,则将违反性质5)。
1、插入
1 RB-INSERT(T, x) 2 y = T.nil 3 z = T.root 4 while z != T.nil do 5 y = z 6 if x.key > y.key then 7 z = z.right 8 else 9 z = z.left 10 x.p = y 11 if y == T.nil then 12 T.root = x 13 else if y.key > x.key then 14 y.left = x 15 else 16 y.right = x 17 x.left = T.nil 18 x.right = T.nil 19 x.color = RED //插入点是红色的 20 RB-INSERT-FIXUP(T, x) //维护红黑树性质
2、维护
插入结点为x,若x.p.color == BLACK,则完事大吉,不需要做任何处理;否则将违反性质4,故需要旋转跳跃我闭着眼~
那如何旋转、如何变色,将需要视情况而定:
情形一:结点x的父节点为左孩子且结点x为左孩子且结点x的叔结点为黑色(或不存在,指向黑色哨兵结点T.nil)。此时仅需①x.p.color = BLACK; x.p.p.color = RED + ②对x.p.p进行右旋操作
情形二:结点x的父节点为左孩子且结点x为右孩子且结点x的叔结点为黑色(或不存在,指向黑色哨兵T.nil)。此类情况经过一定的操作是可以转换为情形一的。此时仅需①令x=x.p(为的是后续能复用情形一的代码) + ②对x进行左旋 + ③对x.p与x.p.p变色 + ④对x.p.p进行右旋操作
情形三:结点x的父结点为右孩子且结点x为右孩子且结点x的叔结点为黑色(或不存在,指向哨兵结点T.nil)。此时仅需令①x.p.color = BLACK; x.p.p.color = RED + ②对x.p.p进行左旋
情形四:结点x的父结点为左孩子且结点x为左孩子且结点x的叔结点为黑色(或不存在,指向哨兵结点T.nil)。此时仅需①令x=x.p(为的是后续能复用情形三的代码)+ ②对x进行右旋 + ③对x.p与x.p.p换色 + ④对x.p.p进行左旋
情形五:结点x的叔结点为红色。此类情况仅需给结点变色即可,①x.p.color = x.p.p.right.color = BLACK; ②x.p.p.color = RED
具体操作如下:
1 RB-INSERT-FIXUP(T, x) 2 while x.p.color == RED do //违反了性质4(x.color亦为RED) 3 if x.p == x.p.p.left then 4 y = x.p.p.right //结点x的叔结点 5 if y.color == BLACK then 6 if x == x.p.right then //情形二 7 x = x.p 8 LEFT-ROTATE(T, x) 9 x.p.color = BLACK //情形一 10 x.p.p.color = RED 11 RIGHT-ROTATE(T, x.p.p) 12 else //y.color == RED //情形五 13 x.p.color = BLACK 14 y.color = BLACK 15 x.p.p.color = RED 16 x = x.p.p 17 18 else //x.p == x.p.p.right 19 y = x.p.p.left //结点x的叔结点 20 if y.color == BLACK then 21 if x == x.p.left then //情形四 22 x = x.p 23 RIGHT-ROTATE(T, x) 24 x.p.color = BLACK //情形三 25 x.p.p.color = RED 26 LEFT-ROTATE(T, x.p.p) 27 else //y.color == RED //情形五 28 x.p.color = BLACK 29 y.color = BLACK 30 x.p.p.color = RED 31 x = x.p.p 32 //while循环外部 33 T.root.color = BLACK
最值操作
获取以结点x为根的子树的值最大/最小的结点
1 //最小值结点 2 TREE-MINIMUM(x) 3 while x.left != T.nil do 4 x = x.left 5 return x 6 7 8 //最大值结点 9 TREE-MAXIMUM(x) 10 while x.right != T.nil do 11 x = x.right 12 return x
前驱&后继操作
获取以结点x为根结点的子树的前驱结点与后继结点
1 //后继结点 2 TREE-SUCCESSOR(x) 3 if x.right != T.nil then 4 return TREE-MINIMUM(x) 5 y = x.p 6 while y != T.nil and x == y.right do 7 x = y 8 y = y.p 9 return y 10 11 12 //前驱结点 13 TREE-PRECESSOR(x) 14 if x.left != T.nil then 15 return TREE-MAXIMUM(x) 16 y = x.p 17 while y != T.nil and x == y.left do 18 x = y 19 y = y.p 20 return y
删除操作
同样与普通二叉搜索树删除结点类似。
1、删除
删除结点x也需要技巧……如何维持搜索二叉树的正确性,也需要视情况而定:
情形一:即将删除的结点x无左子树,此时仅需用结点x的右子树补上即。注意:在删除结点x前需要:
- 缓存x的颜色,记为ori-color;//原因该结点即将被移除,如果是黑色的话,将导致经过该结点的黑高减1,但另外一边不变导致,即破坏了红黑树的性质5
- 记录替补结点y,y = x.right;//原因是如果真的破坏了红黑树的性质,将从替补结点y开始不断修复颜色。(为什么不将y.color变为x的原始颜色后,再修复?原因其一该情形仅判断没有左子树,那可能也不存在右子树,右孩子x.right直接指向哨兵结点T.nil,哨兵结点T.nil无法改变属性~ )故y永远指向即将在树内移动且不能变色的结点。
情形二:即将删除的结点x无右子树,此时仅需用结点x的左子树补上即可。注意:在删除结点x前需要 :
- 缓存x的颜色,记为ori-color;//原因如情形一所述一致
- 记录替补结点y; //原因如情形一所述类似
情形三:即将删除的结点x拥有左右子树,此时操作相对复杂,在删除结点x前需要:
- 找到x的后继结点y;//拥有左右子树的结点要删除的话,那肯定是找前驱或者后继结点补上的……
- 记录y的替补结点w = y.right;//记录y.right,因为y的位置将有y.right顶替。为什么是y.right ? ? y是结点x的后继结点,那y肯定没有左子树…所以y的位置将有y.right(不管是否为T.nil)替补…
- 缓存y的颜色,记为ori-color;//y的位置将由w顶替,w不能变色(像情形一二所述,w可能为T.nil),故需要标记y的原始颜色,后续需要修复时也有依据
- 移植w到y的位置;
- 令y成为结点x右子树根结点,移植y到结点x的位置;//结点y上任
- y.color = x.color;//y.color的颜色变为x.color。为甚?首先y肯定存在且不为T.nil(可改变颜色);可减少变化量,即使移动破坏了红黑树的性质,那也只需要从结点w开始修复即可…
- y = w;//纯粹是想与情形一二保持一致,y永远指向在树内移动又不能变色的结点~~
最后 if ori-color == BLACK then 肯定破坏了红黑树的性质,需要维护;为何ori-color为红色的不需要?原因:(先假设ori-color的原始结点为A)1、A的黑高没有改变;2、A肯定不是根结点,根节点还是黑色。没有任何影响~
具体操作
1 TREE-TRANSPLANT(T, D, N) 2 //将结点N移植到结点D位置 3 if D.p == T.nil then 4 T.root = N 5 else if D == D.p.left then 6 D.p.left = N 7 else 8 D.p.right = N 9 N.p = D.p 10 11 12 //删除结点x 13 RB-DELETE(T, x) 14 ori-color = x.color //标记即将被移除的结点x的颜色,如果该结点为黑色,那么移除后可能会引起黑高不一致 15 if x.left == T.nil then 16 //情形一 17 y = x.right 18 TREE-TRANSPLANT(T, x, y) 19 else if x.right == T.nil then 20 //情形二 21 y = x.left 22 TREE-TRANSPLANT(T, x, y) 23 else 24 //情形三 25 y = TREE-MINIMUM(x.right) 26 ori-color = y.color 27 w = y.right //w即将被移动到结点y的位置,需要标记,目的是解决ori-color为黑色时,移动w到y的位置导致的黑高不一致问题 28 if y.p != x then 29 //将结点y升级为结点x右子树的根结点。 30 TREE-TRANSPLANT(T, y, y.right) 31 y.right = x.right 32 y.right.p = y 33 TREE-TRANSPLANT(T, x, y) 34 y.left = x.left 35 y.left.p = y 36 y.color = x.color 37 y = w 38 if ori-color == BLACK then 39 //ori-color 为黑色,可能破坏了红黑树性质,需要调整维护; 40 RB-DELETE-FIXUP(T, y)
2、维护
仅发生在ori-color为黑色的条件下。
既然移动结点y的操作破坏了红黑树的性质,那肯定需要从结点y开始不断修复;如何操作,也同样需要不同情况不同操作…
y:被移动的结点;w:结点y的兄弟结点(y.p肯定存在左右子树,否则原树将违反性质5)
情形一:y.color == RED。此时需:
- 令y.color = BLACK;//因为已知y.p的左子树或者右子树,比另一边少了一个黑色结点,所以直接变色即可
情形二:y为左孩子,y的颜色为黑色,w的颜色为红色。此时需:
- 令w.color = BLACK,y.p.color = RED;
- 对x.p进行左旋操作;
- 令w重新指向y的兄弟;
- 继续判断w的情况;
情形三:y为左孩子,y与w同为黑色,且w的左右孩子颜色亦为黑色。此时需:
- 改变w的颜色为红色;
- 将y重定向为y的父结点;
- 开启新一轮修复;
情形四:y为左孩子,y与w同为黑色,且w的右孩子为红色。此时要:
- 改变w的颜色为y父结点颜色、w右孩子变成黑色、再将y父结点颜色变为黑色;
- 对y父结点进行左旋操作;
- 令y指向T.root(退出循环)
情形五:y为左孩子,y与w同为黑色,w的右孩子为黑色,w的左孩子为红色。此时要:
- 改变w左孩子的颜色为黑色;
- 改变w的颜色为红色;
- 对w进行右旋;
- 令w重新指向y的兄弟结点;此时就可以用情形四的方法处理了
情形六:y为右孩子,y的颜色为黑色,w的颜色为红色;此时要:
- 改变w的颜色为黑色,改变y.p的颜色为红色;
- 对y.p进行右旋……
情形七:y为右孩子,y与w同为黑色,w的左右孩子均为黑色。此时要:
- 改变w的颜色为红色;
- 令y重定向为y.p;
- 重新检测…
情形八:y为右孩子,y与w同为黑色,w的左孩子为红色。此时要:
- 令w的颜色为y的父结点颜色,令w的左孩子为黑色,令y父结点为黑色;
- 对y父结点进行右旋操作;
- 令y指向T.root(退出循环)
情形九:y为右孩子,y与w同为黑色,w的左孩子为黑色,w的右孩子为红色。此时要:
- 改变w的颜色为红色,改变w右孩子为黑色;
- 对w进行左旋操作;
- 令w重新指向y的兄弟结点;
- 变为了情况八…
具体操作如下
1 RB-DELETE-FIXUP(T, y) 2 while y != T.root and y.color == BLACK do 3 if y = y.p.left then 4 w = y.p.right 5 if w.color == RED then //情形二 6 y.p.color = RED 7 w.color = BLACK 8 LEFT-ROTATE(T, y.p) 9 w = y.p.right 10 if w.left.color == BLACK and w.right.color == BLACK then 11 //情形三 12 w.color = RED 13 y = y.p 14 else 15 if w.right.color == BLACK then 16 //情形四 17 //此时w.left.color必定为红色,因为条件… 18 w.color = RED 19 w.left.color = BLACK 20 RIGHT-ROTATE(T, w) 21 w = y.p.right 22 //情形五 23 w.color = y.p.color 24 y.p.color = BLACK 25 w.right.color = BLACK 26 LEFT-ROTATE(T, y.p) 27 y = T.root 28 else // y == y.p.right 29 w = y.p.left 30 if w.color == RED then //情形六 31 w.color = BLACK 32 y.p.color = RED 33 RIGHT-ROTATE(T, y.p) 34 w = y.p.left 35 if w.right.color == BLACK and w.left.color == BLACK then 36 //情形七 37 w.color == RED 38 y = y.p 39 else 40 if w.left.color == BLACK then //情形八 41 //此时w.right.color肯定为红色,条件限制… 42 w.color = RED 43 w.right.color = BLACK 44 LEFT-ROTATE(T, w) 45 w = y.p.left 46 //情形九 47 w.color = y.p.color 48 y.p.color = BLACK 49 w.left.color = BLACK 50 RIGHT-ROTATE(T, y.p) 51 y = T.root 52 //退出while循环… 53 y.color = BLACK //情形一…