算法导论上对红黑树的评价:
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
二叉查找树
二叉查找树,也称有序二叉树(ordered binary tree),或已排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
- 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 任意节点的左、右子树也分别为二叉查找树。
- 没有键值相等的节点。
因为一棵由n个结点随机构造的二叉查找树的高度为lgn,所以顺理成章,二叉查找树的一般操作的执行时间为O(lgn)。但二叉查找树若退化成了一棵具有n个结点的线性链后,则这些操作最坏情况运行时间为O(n)。
红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。
二叉树的插入
伪代码如下:
TreeInsert(T,z){
y=null;
x=T.root;
while(x!=null){//找到z要插入的位置的父节点y
y=x;
if(z.val<x.val)
x=x.left;
else
x=x.right
}
z.parent=y;
if(y==null)//插入z
T.root=z;
else if(z.val<y.val)
y.left=z;
else
y.right=z;
}
二叉查找树的删除
二叉树结点删除的几种情况,待删除的节点按照儿子的个数可以分为三种:
- 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
- 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
- 有两个儿子。这是最麻烦的情况,因为你删除节点之后,还要保证满足搜索二叉树的结构。其实也比较容易,我们可以选择左儿子中的最大元素或者右儿子中的最小元素放到待删除节点的位置,就可以保证结构的不变。当然,你要记得调整子树,毕竟又出现了节点删除。习惯上大家选择左儿子中的最大元素,其实选择右儿子的最小元素也一样,没有任何差别,只是人们习惯从左向右。这里咱们也选择左儿子的最大元素,将它放到待删结点的位置。左儿子的最大元素其实很好找,只要顺着左儿子不断的去搜索右子树就可以了,直到找到一个没有右子树的结点。那就是最大的了。
二叉查找树的删除代码如下所示:
TreeDelete(T,z){
//case 1
if(z.left==null&&z.right==null){
if(z.parent.left==z) z.parent.left=null;
else z.parent.right=null;
}
//case 2:
if(z.left==null&&z.right==null){
if(z.left!=null)//x为z的不为空的子节点
x=z.left;
else
x=z.right;
if(z.parent==null){//z就是根节点
root=x;
}else if(z=z.parent.left){
z.parent.left=x;
}else{
z.parent.right=x;
}
}
//case 3
x=z.left;
if(x.right==null){//其左节点就是最大的
z.val=x.val;
z.left=x.left;
}else{//其左节点有右节点,找到左子树中最大的一个节点
while(x.right!=null){
x=x.right;
}
z.val=x.val
x.parent.right=null;
}
return z;
}
红黑树
红黑树的5个性质:
- 每个结点要么是红的要么是黑的。
- 根结点是黑的。
- 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
- 如果一个结点是红的,那么它的两个儿子都是黑的。
- 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
(注:上述第3、5点性质中所说的NULL结点,包括wikipedia.算法导论上所认为的叶子结点即为树尾端的NIL指针)
树的旋转知识
当在对红黑树进行插入和删除等操作时,对树做了修改可能会破坏红黑树的性质。为了继续保持红黑树的性质,可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即通过修改树中某些结点的颜色及指针结构,来 达到对红黑树进行插入或删除结点等操作后继续保持它的性质或平衡的目的。
树的旋转分为左旋和右旋
- 左旋:
如上图所示,当在某个结点pivot上,做左旋操作时,我们假设它的右孩子y不是NIL[T],pivot可以为任何不是NIL[T]的左子结点。左旋以pivot到Y之间的链为“支轴”进行,它使Y成为该子树的新根,而Y的左孩子b则成为pivot的右孩子。
LeftRoate(T, x){
y = x.right //定义y:y是x的右孩子
x.right = y.left //y的左孩子成为x的右孩子
if y.left ≠ null //若y的左孩子不为空,令x为y的左孩子的父节点
y.left.p ← x
y.p = x.p //x的父结点成为y的父结点
if x.p == null //若x的父节点为空,说明x是根节点,那么令y为root
then T.root = y
else if x == x.p.left //若x是一个左节点,y成为x父节点的左节点
then x.p.left = y
else x.p.right = y //若x是一个右节点,y成为x父节点的左节点
y.left = x //x作为y的左孩子
x.p = y
}
- 右旋
右旋与左旋类似,就不加赘述
树在经过左旋右旋之后,树的搜索性质保持不变,但树的红黑性质则被破坏了,所以,红黑树插入和删除数据后,需要利用旋转与颜色重涂来重新恢复树的红黑性质。
红黑树的插入和插入修复
红黑树的插入相当于在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作。
RBTreeInsert(T,z){
y=null;
x=T.root;
while(x!=null){
y=x;
if(z.val<x.val)
x=x.left;
else
x=x.right
}
z.parent=y;
if(y==null)//插入z
T.root=z;
else if(z.val<y.val)
y.left=z;
else
y.right=z;
//这之上与平衡二叉树插入方式一致,下面为插入修复
z.left = null;
z.right = null;
z.color = RED
RB-INSERT-FIXUP(T, z)//对结点进行重新着色,并旋转。
}
策略如下:
- 如果插入的是根结点,由于原树是空树,此情况只会违反性质2,因此直接把此结点涂为黑色
- 如果插入的结点的父结点是黑色,由于此不会违反性质2和性质4,红黑树没有被破坏,所以此时什么也不做。
- 如果插入的结点的父结点是红色
- 插入修复情况1:祖父结点的另一个子结点(叔叔结点)是红色
- 插入修复情况2:叔叔节点是黑色,当前节点是其父节点的右子
- 插入修复情况3:叔叔节点是黑色,当前节点是其父节点的左子
RB-INSERT-FIXUP(T, z){
while(z.parent.color==RED){//插入的结点的父节点是红的
if(z.parent==z.parent.parent.left){//若该结点的父节点是一个左子树
y=z.parent.parent.right //y为叔叔结点
if(y.color==RED){// case 1
z.parent.color=BLACK;//将z的父节点设为黑
y.color=BLACK;//将z的叔叔结点设为黑
z.parent.parent.color=RED;// 将z的祖父结点设为红
z=z.parent.parent;//z设为z的祖父结点一直向上迭代
}else if(z==z.parent.right){//case 2
z=z.parent;
LEFT-ROTATE(T,z);//左旋转
}else{//case 3
z.parent.color=BLACK;
z.parent.parent.color=RED;
RIGHT-ROTATE(T,z.parent.parent)l
}
}else{
same as then clause with "right" and "left" exchanged
}
}
T.root.color=BLACK;
}
实例演示:在原树中插入4,找到其父节点为5
此时满足case1:当前结点的父结点是红色,祖父结点的另一个子结点(叔叔结点)是红色。
if(y.color==RED){// case 1
z.parent.color=BLACK;//将z的父节点设为黑
y.color=BLACK;//将z的叔叔结点设为黑
z.parent.parent.color=RED;// 将z的祖父结点设为红
z=z.parent.parent;//z设为z的祖父结点一直向上迭代
}
变换后得到:
当前结点变为7,此时满足case2的条件:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子
else if(z==z.parent.right){//case 2
z=z.parent;
LEFT-ROTATE(T,z);//左旋转
}
变换后得到:
此时当前结点为2,满足case3的条件:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左孩子
else{//case 3
z.parent.color=BLACK;
z.parent.parent.color=RED;
RIGHT-ROTATE(T,z.parent.parent)l
}
最后,把根结点涂为黑色,整棵红黑树便重新恢复了平衡,得到:
红黑树的删除
"我们删除的节点的方法与常规二叉搜索树中删除节点的方法是一样的
- 如果被删除的节点不是有双非空子节点,则直接删除这个节点,用它的唯一子节点顶替它的位置
- 如果它的子节点分是空节点,那就用空节点顶替它的位置
- 如果它的双子全为非空,我们就把它的直接后继节点内容复制到它的位置,之后以同样的方式删除它的后继节点,它的后继节点不可能是双子非空,因此此传递过程最多只进行一次。
红黑树的删除与二差搜索树的删除差不多只不过在删除操作的结尾加上了对颜色的修改
TreeDelete(T,z){
//case 1
if(z.left==null&&z.right==null){
if(z.parent.left==z) z.parent.left=null;
else z.parent.right=null;
}
//case 2:
if(z.left==null&&z.right==null){
if(z.left!=null)//x为z的不为空的子节点
x=z.left;
else
x=z.right;
if(z.parent==null){//z就是根节点
root=x;
}else if(z=z.parent.left){
z.parent.left=x;
}else{
z.parent.right=x;
}
}
//case 3
x=z.left;
if(x.right==null){//其左节点就是最大的
z.val=x.val;
z.left=x.left;
}else{//其左节点有右节点,找到左子树中最大的一个节点
while(x.right!=null){
x=x.right;
}
z.val=x.val
x.parent.right=null;
}
if y.color = BLACK
RB-DELETE-FIXUP(T, x)
return z;
}
“在删除节点后,原红黑树的性质可能被改变,如果删除的是红色节点,那么原红黑树的性质依旧保持,此时不用做修正操作,如果删除的节点是黑色节点,原红黑树的性质可能会被改变,我们要对其做修正操作。那么哪些树的性质会发生变化呢?
- 如果删除节点不是树唯一节点,那么删除节点的那一个支的到各叶节点的黑色节点数会发生变化,此时性质5被破坏。
- 如果被删节点的唯一非空子节点是红色,而被删节点的父节点也是红色,那么性质4被破坏。
- 如果被删节点是根节点,而它的唯一非空子节点是红色,则删除后新根节点将变成红色,违背性质2。”
修复的伪代码如下:
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ▹ Case 1
6 color[p[x]] ← RED ▹ Case 1
7 LEFT-ROTATE(T, p[x]) ▹ Case 1
8 w ← right[p[x]] ▹ Case 1
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ▹ Case 2
11 x ← p[x] ▹ Case 2
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ▹ Case 3
14 color[w] ← RED ▹ Case 3
15 RIGHT-ROTATE(T, w) ▹ Case 3
16 w ← right[p[x]] ▹ Case 3
17 color[w] ← color[p[x]] ▹ Case 4
18 color[p[x]] ← BLACK ▹ Case 4
19 color[right[w]] ← BLACK ▹ Case 4
20 LEFT-ROTATE(T, p[x]) ▹ Case 4
21 x ← root[T] ▹ Case 4
22 else (same as then clause with "right" and "left" exchanged)
23 color[x] ← BLACK
分析技巧:我们从被删节点后来顶替它的那个节点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,我们不是把红黑树的节点加上除红与黑的另一种颜色,这里只是一种假设,我们认为我们当前指向它,因此空有额外一种黑色,可以认为它的黑色是从它的父节点被删除后继承给它的,它现在可以容纳两种颜色,如果它原来是红色,那么现在是红+黑,如果原来是黑色,那么它现在的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。现在只要恢复其它性质就可以了,做法还是尽量向根移动和穷举所有可能性。
如果是以下情况,恢复比较简单:
- 当前节点是红+黑色 解法:直接把当前节点染成黑色,结束此时红黑树性质全部恢复。
- 当前节点是黑+黑且是根节点, 解法:什么都不做,结束。
以下情况需要调用RB-DELETE-FIXUP(T, x),来恢复与保持红黑性质的工作:
- 删除修复情况1:当前节点是黑+黑且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)
- 删除修复情况2:当前节点是黑加黑且兄弟是黑色且兄弟节点的两个子节点全为黑色
- 删除修复情况3:当前节点颜色是黑+黑,兄弟节点是黑色,兄弟的左子是红色,右子是黑色
- 删除修复情况4:当前节点颜色是黑-黑色,它的兄弟节点是黑色,但是兄弟节点的右子是红色,兄弟节点左子的颜色任意
删除修复情况1:当前节点是黑+黑且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)。
解法:把父节点染成红色,把兄弟结点染成黑色,之后重新进入算法(我们只讨论当前节点是其父节点左孩子时的情况)。此变换后原红黑树性质5不变,而把问题转化为兄弟节点为黑色的情况(注:变化前,原本就未违反性质5,只是为了把问题转化为兄弟节点为黑色的情况)。 即如下代码操作:
//调用RB-DELETE-FIXUP(T, x) 的1-8行代码
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ▹ Case 1
6 color[p[x]] ← RED ▹ Case 1
7 LEFT-ROTATE(T, p[x]) ▹ Case 1
8 w ← right[p[x]] ▹ Case 1
变化前:
变化后:
删除修复情况2:当前节点是黑加黑且兄弟是黑色且兄弟节点的两个子节点全为黑色。
解法:把当前节点和兄弟节点中抽取一重黑色追加到父节点上,把父节点当成新的当前节点,重新进入算法。(此变换后性质5不变),即调用RB-INSERT-FIXUP(T, z) 的第9-10行代码操作,如下:
//调用RB-DELETE-FIXUP(T, x) 的9-11行代码
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ▹ Case 2
11 x ← p[x] ▹ Case 2
变化前:
变化后:
删除修复情况3:当前节点颜色是黑+黑,兄弟节点是黑色,兄弟的左子是红色,右子是黑色。
解法:把兄弟结点染红,兄弟左子节点染黑,之后再在兄弟节点为支点解右旋,之后重新进入算法。此是把当前的情况转化为情况4,而性质5得以保持,即调用RB-INSERT-FIXUP(T, z) 的第12-16行代码,如下所示:
//调用RB-DELETE-FIXUP(T, x) 的第12-16行代码
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ▹ Case 3
14 color[w] ← RED ▹ Case 3
15 RIGHT-ROTATE(T, w) ▹ Case 3
16 w ← right[p[x]] ▹ Case 3
删除修复情况4:当前节点颜色是黑-黑色,它的兄弟节点是黑色,但是兄弟节点的右子是红色,兄弟节点左子的颜色任意。
解法:把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点右子染成黑色,之后以当前节点的父节点为支点进行左旋,此时算法结束,红黑树所有性质调整正确,即调用RB-INSERT-FIXUP(T, z)的第17-21行代码,如下所示:
//调用RB-DELETE-FIXUP(T, x) 的第17-21行代码
17 color[w] ← color[p[x]] ▹ Case 4
18 color[p[x]] ← BLACK ▹ Case 4
19 color[right[w]] ← BLACK ▹ Case 4
20 LEFT-ROTATE(T, p[x]) ▹ Case 4
21 x ← root[T] ▹ Case 4
变化前:
变化后:
最后值得一提的是上述删除修复的情况1~4都只是树的局部,并非树的整体全部,且删除修复情况3、4在经过上面的调整后,调整还没结束(还得继续调整直至重新恢复平衡,只是图并没有画出来)。