红黑树的定义
红黑树是一棵二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色,可以是 RED 或 BLACK。通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出两倍,因而是近似于平衡的。
一颗红黑树是满足下面红黑性质的二叉搜索树:
性质一:每个节点或是红色的,或是黑色的。
性质二:根节点是黑色的。
性质三:每个叶节点(NIL)是黑色的。
性质四:如果一个节点是红色的,则它的两个子节点都是黑色的。
性质五:对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
以上内容节选自 “Introduction to Algorithms” 一书,其中文译名为《算法导论》。在实际应用中,红黑树一般作为 STL 中的 map 和 set 容器使用。注意,在性质三中,叶节点表示哨兵节点。
在笔者接下来的论述中,笔者将红黑树(RBTree)的一个节点用五个属性表示,data 表示该节点的权值,left 表示该节点左儿子的下标,right 表示该节点右儿子的下标,parent 表示该节点父节点的下标,colour 表示该节点的颜色。由于性质一,节点的颜色可以由枚举类型定义。我们规定哨兵节点(NIL)的下标为 0,当一个节点的 left 或 right 或 parent 不存在时,相应的属性指向 NIL。同时为了方便起见,规定根节点(RBT_ROOT)为 NIL 的左儿子。
#define NIL 0
#define RBT_ROOT RBTree[NIL].left
enum NodeColour{BLACK,RED};
struct RBTREE_NODE
{
int data,left,parent,right;
NodeColour colour;
}RBTree[MAX_N];
void RBT_CreateTree()
{
RBTree[NIL].colour=BLACK;
RBTree[NIL].left=RBTree[NIL].parent=RBTree[NIL].right=NIL;
RBT_Memory[RBT_SIZE=0]=0;
}
红黑树的单旋转维护
作为维护红黑树性质的一种手段,搜索二叉树的单旋转维护是必要的。不管如何旋转,其权值的中序遍历仍旧是有序的。
void RBT_RotateLeft(int now)
{
int rtc=RBTree[now].right;
if(now==RBTree[RBTree[now].parent].left)
RBTree[RBTree[now].parent].left=rtc;
else
RBTree[RBTree[now].parent].right=rtc;
RBTree[RBTree[rtc].left].parent=now;
RBTree[rtc].parent=RBTree[now].parent;
RBTree[now].right=RBTree[rtc].left;
RBTree[now].parent=rtc;
RBTree[rtc].left=now;
}
void RBT_RotateRight(int now)
{
int ltc=RBTree[now].left;
if(now==RBTree[RBTree[now].parent].left)
RBTree[RBTree[now].parent].left=ltc;
else
RBTree[RBTree[now].parent].right=ltc;
RBTree[RBTree[ltc].right].parent=now;
RBTree[ltc].parent=RBTree[now].parent;
RBTree[now].left=RBTree[ltc].right;
RBTree[now].parent=ltc;
RBTree[ltc].right=now;
}
红黑树的单元素插入维护
对于插入一个元素而言,首先要找到插入的位置。但是如果原本是一棵空树的话,就要加上特判。考虑到红黑树性质的维护问题,不管对新节点赋予何种颜色都有可能破坏红黑性质。若赋予黑色,则肯定破坏性质五(空树例外);若赋予红色,则可能破坏性质四(其父节点颜色为红才破坏)。考虑到性质五较难维护且性质四较不易被破坏,我们将新节点赋予红色。
void RBT_Insert(int num)
{
int fah=RBT_ROOT,now=RBT_NewNode();
RBTree[now].data=num;
RBTree[now].colour=RED;
RBTree[now].left=RBTree[now].right=NIL;
if(RBT_ROOT==NIL)
{
RBT_ROOT=now;
RBTree[RBT_ROOT].parent=NIL;
RBTree[RBT_ROOT].colour=BLACK;
return;
}
while(true)
if(RBTree[now].data<RBTree[fah].data)
{
if(RBTree[fah].left==NIL)
{
RBTree[fah].left=now;
RBTree[now].parent=fah;
break;
}
fah=RBTree[fah].left;
}
else
{
if(RBTree[fah].right==NIL)
{
RBTree[fah].right=now;
RBTree[now].parent=fah;
break;
}
fah=RBTree[fah].right;
}
RBT_InsertMaintain(now);
}
在插入之后,我们就要对这颗红黑树进行维护,以确保它仍然满足红黑性质。在维护过程中我们保证其他性质不被破坏而将 now 上移,即将两红相邻的状态上移至根节点,或在上移过程中恢复了性质四。当我们上移至根节点时,由于根节点恒为黑,相当于破坏了性质二而恢复了性质四,这时我们只需将根节点的颜色设为黑色即可。注意,我们只在 now 的父节点颜色为红时才进行维护。我们将插入维护分为以下三种情形:
情形一:now 的叔节点颜色为红
将 now 的父节点和叔节点的颜色改为黑色,将 now 的祖节点的颜色改为红色,将 now 上移两层至其祖节点,跳过后面情形判断,继续循环。
情形二:now 的父节点为 now 的祖节点的右儿子且 now 为 now 的父节点的左儿子或其对称情况(即 now、now 的父节点、now 的祖节点不共线)
将 now 的父节点单旋转以共线之,将 now 下移一层至其原父节点,继续后面情形判断。
情形三:不满足情形一者
将 now 的父节点的颜色改为黑色,将 now 的祖节点的颜色改为红色,将 now 的祖节点单旋转以让 now 成为 now 的原祖节点的兄节点,结束循环。
int RBT_QueryGrandparent(int now)
{
return RBTree[RBTree[now].parent].parent;
}
int RBT_QueryUncle(int now)
{
int gpa=RBT_QueryGrandparent(now);
if(gpa!=NIL)
if(RBTree[now].parent==RBTree[gpa].left)
return RBTree[gpa].right;
else
return RBTree[gpa].left;
return NIL;
}
void RBT_InsertMaintain(int now)
{
int gpa,ucl;
while(RBTree[RBTree[now].parent].colour==RED)
{
gpa=RBT_QueryGrandparent(now);
ucl=RBT_QueryUncle(now);
if(RBTree[ucl].colour==RED)
RBTree[RBTree[now].parent].colour=RBTree[ucl].colour=BLACK,
RBTree[now=gpa].colour=RED;
else
if(RBTree[now].parent==RBTree[gpa].left)
{
if(now==RBTree[RBTree[now].parent].right)
RBT_RotateLeft(now=RBTree[now].parent);
RBTree[gpa].colour=RED;
RBTree[RBTree[now].parent].colour=BLACK;
RBT_RotateRight(gpa);
}
else
{
if(now==RBTree[RBTree[now].parent].left)
RBT_RotateRight(now=RBTree[now].parent);
RBTree[gpa].colour=RED;
RBTree[RBTree[now].parent].colour=BLACK;
RBT_RotateLeft(gpa);
}
}
RBTree[RBT_ROOT].colour=BLACK;
}
分析可知,循环只有在情形一满足的情况下才持续进行,而情形一只是颜色的变动而不涉及单旋转。当进行单旋转的情形二或情形三满足时,循环结束,所以插入操作最多进行两次单旋转。
事实上,我们在进行单元素插入维护的时候,要领就是把节点的考虑缩小在 now、now 的父节点、now 的祖节点和 now 的叔节点四个节点之内。由于 now 和 now 的父节点的颜色必为红(否则维护已经完成),所以 now 的祖节点的颜色必为黑(否则红黑性质早已破坏),这样我们就可以只用讨论 now 的叔节点的颜色来分情形维护了。
红黑树的单元素删除维护
红黑树的单元素删除比较复杂,首先要找到待删除元素的位置,若不存在直接结束即可。然后,和一般的二叉搜索树相同,我们需要找到以待删除元素为根的子树中的前驱和后继作为替代节点。易知这样的替代节点必然最多只能有一个子节点,这样我们只需将待删除元素位置的权值改为替代节点的权值而将替代节点删去,以其子节点代替原先的位置即可。
但是,以上做法有可能破坏红黑性质。当替代节点的颜色为红色时,红黑性质得以保持;但当替代节点为黑色时,性质四和性质五都有可能被破坏。然而,稍作分析我们发现,性质四被破坏当且仅当替代节点的颜色为黑色且替代节点的子节点和替代节点的父节点的颜色为红色,此时性质五也必然一同被破坏,但我们此时只需将替代节点的子节点的颜色改为黑色便可恢复红黑性质。于是,我们只需考虑替代节点的颜色为黑色且性质五被破坏的情形。因为即使性质四被一同破坏(性质四无法单独被破坏),我们在恢复性质五的过程中也会一并将性质四恢复。
考虑恢复性质五的方法,其实质是 now 所在子树少一个黑色节点。我们可以通过变换,将 now 所在子树缺少的那个黑色节点补回来,或者将 now 节点的兄节点所在子树也减少一个黑色节点,然后将 now 上移至其父节点。当 now 上移至根节点时,红黑性质也便恢复了。但是,这样的做法有一个问题,当替代节点无子节点时,now 岂不为 NIL?这样的做法不仅会使代码的可读性大幅度降低,而且还增加了思维难度。
在这里,我们有一种巧妙的思维方法可以解决这个难题。在找到替代节点并完成权值的替代后,我们先不删替代节点,而是先通过维护将替代节点所在子树增加一个黑色节点。完成这种操作的方法与上文所述类似,但更为简洁。通过变换将 now 的兄节点减少一个黑色节点,将 now 上移至其父节点,判断此时 now 的颜色是否为黑色,是则继续循环,否则退出循环并将 now 的颜色赋为黑色。在完成维护后再删去替代节点,则剩下的红黑树必然满足红黑性质。
void RBT_Delete(int num)
{
int del,now=RBT_ROOT;
while(now!=NIL&&num!=RBTree[now].data)
if(num<RBTree[now].data)
now=RBTree[now].left;
else
now=RBTree[now].right;
if(now==NIL)
return;
if(RBTree[now].left!=NIL)
{
for(del=RBTree[now].left;RBTree[del].right!=NIL;del=RBTree[del].right);
RBTree[now].data=RBTree[del].data;
now=RBTree[del].left!=NIL?RBTree[del].left:del;
}
else if(RBTree[now].right!=NIL)
{
for(del=RBTree[now].right;RBTree[del].left!=NIL;del=RBTree[del].left);
RBTree[now].data=RBTree[del].data;
now=RBTree[del].right!=NIL?RBTree[del].right:del;
}
else
del=now;
if(RBTree[del].colour==BLACK)
RBT_DeleteMaintain(now);
RBT_DeleteNode(del);
}
与插入操作不同,在这里删除维护有以下四种情形:
情形一:now 的兄节点颜色为红
将 now 的父节点的颜色改为红色,将 now 的兄节点的颜色改为黑色,将 now 的父节点单旋转以让 now 的父节点成为 now 的原兄节点的子节点,继续后面情形判断。
情形二:now 的兄节点的两个子节点的颜色都为黑
将 now 的兄节点的颜色改为红色,将 now 上移一层至其父节点,跳过后面情形判断。当 now 的颜色为黑色时继续循环,当 now 的颜色为红色时结束循环。
情形三:now 的兄节点的两个子节点的颜色不同且其中的红色节点、now 的兄节点和 now 的父节点不共线
将 now 的兄节点单旋转以共线之,继续后面情形判断。
情形四:不满足情形二者
将 now 的兄节点的颜色改为 now 的父节点的颜色,将 now 的父节点的颜色改为黑色,将 now 的父节点单旋转以让 now 的父节点成为 now 的原兄节点的子节点,结束循环。根据上文所述,为了方便起见,我们将 now 的父节点的颜色改为红色,将 now 上移一层至其父节点,使其循环判断结束后将 now 的颜色赋为黑色。
int RBT_QuerySibling(int now)
{
if(now!=NIL&&RBTree[now].parent!=NIL)
if(now==RBTree[RBTree[now].parent].left)
return RBTree[RBTree[now].parent].right;
else
return RBTree[RBTree[now].parent].left;
return NIL;
}
void RBT_DeleteMaintain(int now)
{
int sbl;
while(RBTree[now].parent!=NIL&&RBTree[now].colour==BLACK)
{
sbl=RBT_QuerySibling(now);
if(RBTree[sbl].colour==RED)
{
RBTree[RBTree[now].parent].colour=RED;
RBTree[sbl].colour=BLACK;
if(now==RBTree[RBTree[now].parent].left)
RBT_RotateLeft(RBTree[now].parent);
else
RBT_RotateRight(RBTree[now].parent);
sbl=RBT_QuerySibling(now);
}
if(RBTree[RBTree[sbl].left].colour==BLACK
&&RBTree[RBTree[sbl].right].colour==BLACK)
RBTree[sbl].colour=RED;
else
if(now==RBTree[RBTree[now].parent].left)
{
if(RBTree[RBTree[sbl].left].colour==RED
&&RBTree[RBTree[sbl].right].colour==BLACK)
RBT_RotateRight(sbl),
RBTree[sbl].colour=RED,
RBTree[sbl=RBTree[sbl].parent].colour=BLACK;
RBTree[sbl].colour=RBTree[RBTree[now].parent].colour;
RBTree[RBTree[now].parent].colour=RED;
RBTree[RBTree[sbl].right].colour=BLACK;
RBT_RotateLeft(RBTree[now].parent);
}
else
{
if(RBTree[RBTree[sbl].left].colour==BLACK
&&RBTree[RBTree[sbl].right].colour==RED)
RBT_RotateLeft(sbl),
RBTree[sbl].colour=RED,
RBTree[sbl=RBTree[sbl].parent].colour=BLACK;
RBTree[sbl].colour=RBTree[RBTree[now].parent].colour;
RBTree[RBTree[now].parent].colour=RED;
RBTree[RBTree[sbl].left].colour=BLACK;
RBT_RotateRight(RBTree[now].parent);
}
now=RBTree[now].parent;
}
RBTree[now].colour=BLACK;
}
分析可知,只有当满足情形二且 now 的父节点的颜色为黑色时才持续循环。但是,情形二不涉及旋转操作,所以时间耗费时可以接受的。而进行循环次数最多的情况就是进入情形一、情形三和情形四然后结束循环,这样所以删除操作最多进行三次单旋转。
和插入操作类似,我们在进行单元素删除维护时,就是把节点的考虑缩小在 now、now 的父节点、now 的兄节点和 now 的兄节点的两个子节点五个节点之内。由于性质四未被破坏,所以情况较少。加之很多情况的变换方法类似,故笔者在这里仅整理了四种情形,力图简略而不失理解。
红黑树的空间回收优化
在删除操作中,我们将红黑树中的一些节点从树上删去了,我们的红黑树再也无法找到它们。但是,加上空间回收的内存池方法,我们便可以让这些空间重新利用起来。其方法较简单,只需在新建节点和删除节点的时候略作修改即可。
int RBT_Memory[MAX_N],RBT_SIZE;
int RBT_NewNode()
{
if(RBT_Memory[0]>0)
return RBT_Memory[RBT_Memory[0]--];
return ++RBT_SIZE;
}
void RBT_DeleteNode(int del)
{
int son;
son=RBTree[del].left!=NIL?RBTree[del].left:RBTree[del].right;
if(del==RBTree[RBTree[del].parent].left)
RBTree[RBTree[del].parent].left=son;
else
RBTree[RBTree[del].parent].right=son;
RBTree[son].parent=RBTree[del].parent;
RBTree[del].data=RBTree[del].left=RBTree[del].parent=RBTree[del].right=NIL;
RBTree[NIL].parent=NIL;
RBT_Memory[++RBT_Memory[0]]=del;
}
红黑树的直观输出
红黑树作为一种较为复杂的数据结构,难以直观地看出代码的正误是一件麻烦的事,就算是大数据输出验证也因为红黑树独有的颜色标识无法表示而举步维艰。事实上,我们大可不用一些作图的库函数,仅凭二叉树的凹入表和颜色加以区分。设置字体和背景颜色的命令在头文件 windows.h 下。
#include"stdio.h"
#include"windows.h"
#define OUTPUT_BLACKWHITE SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), \
BACKGROUND_INTENSITY|BACKGROUND_RED|BACKGROUND_GREEN|BACKGROUND_BLUE)
#define OUTPUT_REDWHITE SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), \
FOREGROUND_INTENSITY|FOREGROUND_RED| \
BACKGROUND_INTENSITY|BACKGROUND_RED|BACKGROUND_GREEN|BACKGROUND_BLUE)
void RBT_Output(int now,int dep)
{
if(now==NIL)
return;
RBT_Output(RBTree[now].left,dep+1);
for(int i=0;i<dep;i++)
printf(" ");
if(RBTree[now].colour==BLACK)
OUTPUT_BLACKWHITE;
else
OUTPUT_REDWHITE;
printf("%d\n",RBTree[now].data);
RBT_Output(RBTree[now].right,dep+1);
}
调用命令:
system("color F0");
RBT_Output(RBT_ROOT,0);
红黑树的总结
如果说,有一个网站里的有关红黑树的讲解可以媲美网络上绝大多数佼佼之辈的笔记的话,那么这个网站就一定是
https://en.wikipedia.org/wiki/Red%E2%80%93black_tree
本文中有关红黑树的单元素插入删除维护的插图来源自以上网址,而有关红黑树的单旋转的插图节选自《算法导论》。
红黑树虽然在平衡性能上比 AVL 树较差,但其维护时间费用小使得红黑树的应用比 AVL 树更广。和高费时维护的 AVL 树、功能不全的 Tire 树、随机的 Splay 树不同的,大概就是在各个方面都各有所长的红黑树吧。