树
《算法导论》中第一次引入树这种数据结构是以二叉堆的形式出现的,并且在附录中在图的基础上给出了树在数学上的定义。如果从链表的观点出发,那么相当于是放宽了有序的要求,即允许两个不同位置的元素有相等的序。
对于序为的节点来说,可以指向多个序为的节点,相应的后者称为前者的孩子,前者称为后者的父节点。其最大序即为树的高度。(下图中数字表示的是节点的代数,而非值)
在上图中,0节点的左右两个节点分别为其左子节点和右子节点,反过来0节点也是这两个子节点的父节点;左侧的1节点的两个2节点也分别为其左子节点和右子节点,以此类推。
很容易发现,在一个树中,只有0节点没有父节点,这个节点被称为根节点。
二叉搜索树
二叉搜索树要求父节点大于等于其左子节点及其所有子节点,而小于等于其右子节点及其所有子节点,如下图所示
初始化
如果想要在这个树中查询任意一个值,其最坏的情况也无非是查询到最下面的点,进行的比较次数为树的高度。由于这是二叉树,设树的元素个数为,则理想情况下树的高度不大于。
对于二叉搜索树中的每个父节点最多子节有两个子节点,树中任意节点有三个指针,分别指父向节点、左子节点和右子节点。其中,根节点没有父节点。C语言实现为
//cTrees.c
typedef struct TREENODE
{
struct TREENODE *father;
struct TREENODE *left;
struct TREENODE *right;
int value;
}tNode;
现在,我们迫切地希望能够实现一个二叉搜索树,但正如此前所接触的线性数据结构一样,生成树的过程也必然始于添加节点。
对于一个已有的二叉搜索树而言,当我们插入一个新节点的时候,应该比较新节点与当前节点的值,如果大于当前节点,则比较新节点与当前节点右子节点的值;如果小于当前节点,则比较新节点与当前节点左子节点的值。如果下一个将要比较的节点不存在,那么正好可以把新节点插进来。
void insertNode(tNode* root, int val){
tNode* new = (tNode*)malloc(sizeof(tNode));
new->value=val;
new->left=NULL;
new->right=NULL;
while (TRUE){
if (root->value<val)
if(root->right!=NULL)
root=root->right;
else{
//若右子节点不存在,则新节点成为其右子节点
new->father = root;
root->right = new;
return; //赋值之后函数结束
}
else //左边的操作与右边相同
if (root->left!=NULL)
root=root->left;
else{
new->father=root;
root->left=new;
return;
}
}
}
当能够生成二叉搜索树之后,我们更迫切地想知道这个二叉搜索树生成的对不对,所以需要打印这个二叉搜索树
//打印二叉搜索树,输入为节点和节点序
void printBST(tNode* root,int start){
printf("the %dth node is %d\n",start,root->value);
//如果当前节有子节点,则继续打印这个子节点和节点序
if (root->left!=NULL)
printBST(root->left,start+1);
if (root->right!=NULL)
printBST(root->right,start+1);
}
最终在主函数中进行验证
int main(){
tNode* root;
root->left=NULL;
root->right=NULL;
root->value=10; //以上初始化根节点
int init[10]={1,11,5,12,2,4,19,11,8,7};
for (int i = 0; i < 10; i++)
insertNode(root,init[i]);
printBST(root,0);
return 0;
}
其结果为
PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe
the 0th node is 10
the 1th node is 1 //第一代节点,10的左子节点
the 2th node is 5 //第二代节点,1的右子节点
the 3th node is 2 //第三代节点,5的左子节点
the 4th node is 4 //第四代节点,2的右子节点
the 3th node is 8 //第三代节点,5的右子节点
the 4th node is 7 //第四代节点,8的左子节点
the 1th node is 11 //第一代节点,10的又子节点
the 2th node is 11 //第二代节点,11的左子节点
the 2th node is 12 //第二代节点,11的右子节点
the 3th node is 19 //第三代节点,12的右子节点
PS E:\Code\AlgC>
我们生成的树为
可见的确符合二叉搜索树的规则。那么接下来我们需要为这棵二叉树添加新的功能,即搜索节点与删除节点。其中,搜索功能与插入功能如出一辙,区别只是在于,我们不需要重复插入,我们只需将这个值的指针返回即可。同时,循环判定也变为while(root->value!=val)
//通过节点的值搜索节点地址,root为根节点
tNode* searchBST(tNode* root, int val){
while (root->value!=val)
{
if (root->value<val && root->right!=NULL)
root=root->right;
else if (root->value>val && root->left!=NULL)
root=root->left;
else
return FALSE;
}
return root;
}
删除节点
相比之下,删除节点显得更加复杂一些,因为此时将涉及到其父节点、左子节点、右子节点以及兄弟节点之间的大小关系。
如果被删除的节点没有子节点,那当然皆大欢喜,只需将其父节点指向被删除节点的指针变成NULL
即可;如果只有一个子节点,也并不麻烦,只需指向被删除节点的指针指向这个子节点。
然而,如果有两个子节点,那么由于父节点必须大于左子节点而小于右子节点,所以取代被删除节点的一可以是左子节点,也可以是右子节点。区别在于,若是右子节点取代该节点,则左子节点为新父节点的左子节点;若是左子节点取代父节点,则右子节点仍为新父节点的右子节点。
所以,二选其一即可,交换当前节点与其右子节点,然后删除交换后的右子节点即可,如果交换后的右子节点仍然有两个子节点,则继续交换,直到能够删除为止。这里其实有一个巧妙的默认,即默认为待删除节点无论处于什么位置都是合法的,毕竟这个节点最终将被删除掉。
//删除节点的值,root为根节点,delNode为待删除节点
void deleteNode(tNode* delNode){
if(delNode->left==NULL&&delNode->right==NULL){
if(delNode->value>pNode->value)
pNode->right=NULL;
else
pNode->left=NULL;
}
else if(delNode->left!=NULL&&delNode->right!=NULL){
int val = delNode->value;
//交换当前节点与右节点的值
delNode->value = delNode->right->value;
delNode->right->value=val;
deleteNode(delNode->right);//删除右节点
}
else{
tNode* pNode = (delNode->left==NULL) \
? delNode->right : delNode->left;
delNode->value = pNode->value;
delNode->right = pNode->right;
delNode->left = pNode->left;
}
}
验证一下
int main(){
tNode* root;
root->left=NULL;
root->right=NULL;
root->value=10;
int init[10]={1,11,5,12,2,4,19,11,8,7};
for (int i = 0; i < 10; i++)
insertNode(root,init[i]);
tNode* sNode=searchBST(root,5);
deleteNode(sNode);
printBST(root,0);
return 0;
}
结果为
PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe
the 0th node is 10
the 1th node is 1
the 2th node is 8
the 3th node is 2
the 4th node is 4
the 3th node is 7
the 1th node is 11
the 2th node is 11
the 2th node is 12
the 3th node is 19
示意图为
旋转节点
二叉搜索树有一个问题,如果我们在对其进行初始化的时候,输入的是,那么这个所谓的二叉树可能并不会产生叉,而是径直变成一个只有右子节点的链表。
由于二叉树的时间复杂度与其树高是成正比的,所以不分叉的二叉树也会失去时间复杂度上的优势。所以,如果能够控制二叉树的高度,使之横向分布尽量均匀,就能够有效提高其性能。
最直观的想法是,假设是的右子节点,而的左子节点的家族人丁稀薄,的右子节点子系繁多,那么如果把的子系过继给,或者干脆取代,父子关系逆转,必定能使得整个树变得更加均匀。
现考虑这五个节点,其中是的左右节点,是的左右节点。那么这些节点之间必然存在关系。如果希望变为的父节点,那么必然是的左子节点。此时将多出一个节点,必须过继给,又因为,所以只能过继左子节点。
这里可以考虑插入一个中间步以便于理解
可见多了一个节点正好可以过继给,成为其左子节点。则过继之后的节点关系变成
可见这个过程并没有改变二叉搜索树的性质,但是在长于的情况下,能够有效降低树的高度。
因为的转置过程就像旋转一样,所以这个操作叫做旋转,又因为父节点变成了子节点的左子节点,所以叫左旋,其逆过程就是右旋。但本文中并不提倡左旋右旋这种故作高深的提法,而是提倡旋转两个节点这种说法。
总之可能是翻译的脑补能力很强,所以起了这么个奇葩的名字。其实这个操作的本质就是这五个点的重新排布而已,硬把这种重新排布命名成旋转我也是醉了,就算翻译成也要比旋转更加贴切,也能减少理解上的困难。
旋转操作落实到算法上,其实并不需要考虑,但需要考虑考虑父节点指针的变化。即整个操作过程无非是以及父节点的指针变化而已,传统的旋转实现为
#define RIGHT 1
#define LEFT 0
//树节点的经典旋转操作,flag为LEFT时左旋,RIGHT时右旋
void rotNode(tNode *xNode, int flag){
tNode *yNode;
if (flag==LEFT){
yNode = xNode->right;
xNode->father->right = yNode;//y成为x父节点的右子节点
}else{
yNode = xNode->left;
xNode->father->left = yNode;
}
yNode->father = xNode->father; //x的父节点成为y的父节点
xNode->father = yNode; //y成为x的父节点
if (flag == LEFT){
yNode->left->father = xNode; //y左子节点过继给x
xNode->right = yNode->left;
yNode->left = xNode;
}else{
yNode->right->father = xNode;
xNode->left = yNode->right;
yNode->right = xNode;
}
}
将主函数中删除操作代码换成rotNode(sNode,LEFT);
,其结果为
PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe
the 0th node is 10
the 1th node is 5
the 2th node is 1
the 3th node is 2
the 4th node is 4
the 2th node is 8
the 3th node is 7
the 1th node is 11
the 2th node is 11
the 2th node is 12
the 3th node is 19
可见5的确成为了1的父节点,而2过继给了1,树高并没有变化,是因为原5的左右子族的长度相等。从代码的角度来说,替换意义上的旋转操作可以更加简洁,而且更有利于理解。
//替换意义上的旋转操作,sNode为子节点,pNode为父节点
void turnNode(tNode *sNode, tNode *pNode){
if(sNode==pNode->right){
sNode->left->father = pNode;
pNode->right = sNode->left;
sNode->left = pNode;
}else{
sNode->right->father = pNode;
pNode->left = sNode->right;
sNode->right = pNode;
}
sNode->father = pNode->father;
pNode->father = sNode;
}
红黑树
调整节点
有了旋转操作,那么问题便成了何时旋转。最直观的方案是,让每个节点都包含辈分信息,然后想办法让每一个家族的辈分相差不要太过悬殊。这种方案的问题是,如果改变一个父节点的辈分,那么这个父节点的所有子孙,将都会受到影响。
所以,我们需要找到某中共衡量树高的某个参数,并且这个参数易于保持。由于树的节点数目并不固定,所以不同子孙所构成链表的长度也必然不等,要求每个家族的最小辈分完全相等是不现实的,唯一能够做到的是,让每个家族在抽离一些特殊的子女之后,达到辈分相等。
红黑树便是本着这样一种思维,这里要求任意一个父节点到其最后一代节点的所有简单路径中,包含相同数目的黑色节点。考虑到父节点到其后低啊的所有简单路径不可能包含相同的节点,所以要在黑色节点之间插入红色节点,以保证黑色节点数目相等。
但是,红色节点不能乱插,并且必须要少于黑色节点,所以要求红色父节点的两个子节点都为黑色。
首先,定义一下红黑树的节点。
#define RED 0
#define BLACK 1
typedef struct rbNODE
{
struct rbNODE* father;
struct rbNODE* left;
struct rbNODE* right;
int value;
int color;
}rbNode;
现在,就可以尝试生成一棵红黑树。像二叉搜索树一样,首先要有一个根节点,由于根节点对于所有子孙节点都是唯一的,所以选则黑色即可。
便于指导后面的操作,将红黑树的两点要求列在此处
- 任意父节点到其最后一代孙节点的所有简单路径中,黑色节点相同数目。
- 红节点的左右子节点均为黑色。
第一条性质也可以写为等价形式:任何一个末代孙节点到根节点的简单路径中,黑色节点数目相同。或者说,任何两个末代孙节点抵达任意一个相同的祖节点的简单路径中,黑色节点数目相同。
- 叔节点与父节点都为红色
然后,如果向已有的红黑树中插入新的节点,由于第一条规则,我们优先考虑红色。如果这个的父节点也是红色,那就尴尬了,违反了第二条规则,所以需要把变成黑色。但变成黑色之后,这条路径就比其他路径多了一条黑色节点。
这时如果的兄弟节点、的叔叔是红色节点就好办了,可以将也变成黑色,然后将的父节点变成红色。这样,的所有子系就得到了统一,从而整棵树都得到了统一。唯一可能麻烦的是,和其父节点可能会违反第二条规则,但这已经是的父辈和祖辈之间的事情了,重复调用即可。
- 叔节点为黑色
然而,如果是黑色的,那么问题会有些麻烦,这里可以考虑一下旋转操作,如下图所示
假设的子系均符合红黑树的要求,比较旋转前后的各条子系
旋转前 | 旋转后 |
---|---|
可见,如果均为红色,则旋转前后黑点的数目并不会发生变化;如果为黑色,则这条子系减少一个黑节点;如果为黑色,则这条子系增加一个黑节点。
总结一下,即两个红色节点的旋转操作不会改变子系的黑色节点数目;红父与右黑子的旋转,会使红父的左子节点的子系增加一个黑色节点;黑父与右红子的旋转,会使红子的右节点减少一个黑色节点。这意味着当父子节点均为红色时,我们就可以大胆地使用旋转操作而不必担心出乱子。
当父节点和子节点都为红色,且的叔节点为黑色时,我们可以尝试旋转一下节点,但旋转之后并不会改变二者的颜色,二者仍旧不满足第二条规则。但由于是红色,那么的父亲一定为黑色,而的兄弟节点也为黑色,所以只需变成黑色,让变成红色就能够满足第二条规则了。
但这里又出现了新的问题,满足第二条规则之后,的子系必然因为的变色而少了一个黑色节点。考虑到二者的颜色,现在将这两个节点再旋转一次,正好能够使得子系增加一个节点,至此红黑树又重新满足了要求。
总结一下,如果
- 叔节点存在且为红色,则将父节点和叔节点同时设为黑色,将祖父节点设为红色,然后将指针指向祖父节点。
若叔节点不存在或为黑色,则
- 若插入节点与父节点在同侧(例如,插入节点为左节点,父节点也为左节点),则将父节点设为黑色,将祖父节点设为红色,旋转。
- 若插入节点与父节点在异侧,则旋转和插入节点,然后将指针移向,此时与其父节点成为同侧节点。
//调整红黑树的节点
//调整红黑树的节点
void adjustRBT(rbNode *node){
rbNode *pNode = node->father; //父节点
rbNode *qNode; //叔节点
while (pNode->color==RED){
int flag = pNode == pNode->father->left
? LEFT : RIGHT;
qNode = flag==LEFT ? pNode->father->right
: pNode->father->left; //叔节点
//如果叔节点存在且为红色
if (qNode!=NULL || qNode->color==RED){
pNode->color = BLACK;
qNode->color = BLACK;
pNode->father->color = RED;
node = pNode->father;
pNode = node->father;
}
else{
if(flag != (node==pNode->left ? LEFT : RIGHT)){
turnRbNode(node,pNode);//此时插入节点与父节点在异侧
node = pNode;
pNode = node->father;
}//执行完此操作后,变为同侧
pNode->color=BLACK;
pNode->father=RED;
turnRbNode(pNode,pNode->father);
}
}
}
初始化
红黑树的根节点颜色并不会影响红黑树的第一条性质,但如果红黑树的根是红色的,那么其左子节点和右子节点必须同时为黑色。所以,当根节点为黑色时显然对其后代颜色的影响更小,所以选取根色为黑。
此前所定义的二叉树的插入操作对红黑树完全有效,但是需要额外添加节点颜色。为此,可以从二叉树的插入函数中提取出新节点的指针,并为这个指针赋予颜色,然后对这个指针的颜色进行调整。
当然,在真正启动初始化程序之前,最好还是检查一下此前的算法的漏洞。红黑树的核心算法adjustRBT
中所引用的旋转操作其实隐藏着一个很大的bug,即其默认在树中间进行操作,所涉及到的所有的节点元素都不为NULL
,所以一旦涉及到根节点或者末代节点,就必然会引发灾难。所以,必须在变化之前进行节点判断
//打印红黑树,使之能够显示红黑特性
void printRBT(rbNode *root, int start){
printf("the %dth node : %d with %d\n", start, root->value, root->color);
if (root->left != NULL)
printRBT(root->left, start + 1);
if (root->right != NULL)
printRBT(root->right, start + 1);
}
//旋转红黑树的节点,sNode是被旋转的子节点
//root为根节点,输出为旋转后的根节点
rbNode* turnRbNode(rbNode *root, rbNode *sNode){
rbNode* pNode = sNode->father; //被旋转的父节点
if(sNode==pNode->right){ //s为右子节点
if(sNode->left!=NULL)
sNode->left->father = pNode;//s的左子节点过继给pNode
pNode->right = sNode->left; //p接收s的左子节点
sNode->left = pNode; //p成为s的左子节点
}else{
if (sNode->right!=NULL)
sNode->right->father = pNode;
pNode->left = sNode->right;
sNode->right = pNode;
}
sNode->father = pNode->father; //sNode过继给pNode的父节点
pNode->father = sNode; //pNode和sNode父子逆转
if (sNode->father==NULL) //若pNode为根节点
return sNode;
if (pNode==pNode->father->right)
sNode->father->right = sNode;
else
sNode->father->left = sNode;
return root;
}
//红黑树的插入算法
rbNode* insertRbNode(rbNode *root, int val){
rbNode *new = (rbNode *)malloc(sizeof(rbNode));
new->value = val;
new->left = NULL,new->right = NULL;
new->color = RED;
rbNode *tmpRoot = root; //保护root
rbNode *temp;
while (temp = root->value < val
? root->right : root->left,temp!=NULL)
root = temp;
new->father = root;
if (root->value < val)
root->right = new;
else
root->left = new;
return adjustRBT(tmpRoot,new);
}
主函数为
//红黑树插入算法
int main(){
rbNode Root = {NULL,NULL,NULL,11,1};
rbNode* root = &Root;
int init[7]={2,14,1,7,15,5,8};
for (int i = 0; i < 7; i++){
root = insertRbNode(root,init[i]);
}
root = insertRbNode(root,4);
printRBT(root,0);
return 0;
}
输出为
PS E:\Code\AlgC> .\a.exe
the 0th node : 7 with 1
the 1th node : 2 with 1
the 2th node : 1 with 1
the 2th node : 5 with 1
the 3th node : 4 with 0
the 1th node : 11 with 1
the 2th node : 8 with 1
the 2th node : 14 with 1
the 3th node : 15 with 0
这里选取的参数与《算法导论》中一致,可以通过调试看出节点以及节点颜色的变化过程,与《算法导论》中所列出的基本一致,所以就不画图了。
删除节点
回顾一下二叉搜索树的节点删除操作,可以发现,如果被删除节点有两个节点,则这个节点将与其子节点的值进行交换。这种交换终止于交换后的子节点不多于一个子节点。
当然,这个过程可以改得更加激进一些,即只要被删除节点仍有子节点,那么就将该节点与子节点的值进行交换,然后将指针指向子节点,直到指针指向末代节点,然后删除。
如果将这种操作挪用到红黑树上,那么在值交换的过程中,并不必交换节点颜色,于是只有最终删除末代节点的时候,才需要考虑节点颜色。
然而,末代节点被删除将导致末代节点这条世系彻底消失,所以,无论末代节点的颜色如何,都不会改变其他世系的黑高,所以我们惊奇地发现,别穿得难乎其难的红黑树删除节点操作,竟然简单的让人难以置信。
//红黑树查询,root为根节点,val为待查询值
//返回值为节点的指针
rbNode* searchRBT(rbNode *root, int val){
if(root->value==val)
return root;
if(root->value<val && root->right!=NULL)
return searchRBT(root->right,val);
else if(root->value>val && root->left!=NULL)
return searchRBT(root->left,val);
else
return FALSE;
}
//红黑树删除节点,输入为待删除节点指针
void deleteRbNode(rbNode* dNode){
rbNode *pNode = dNode->father;
if (dNode->left == NULL && dNode->right == NULL){
if (dNode==pNode->right)
pNode->right = NULL;
else
pNode->left = NULL;
}
else{
//如果左子节点存在,则pNode为dNode的左子节点,否则为右子节点
pNode = (dNode->left==NULL) ? dNode->right : dNode->left;
int val = dNode->value;
dNode->value = pNode->value;
pNode->value = val;
deleteRbNode(pNode);
}
}
//主函数
int main()
{
rbNode Root = {NULL,NULL,NULL,11,1};
rbNode* root = &Root;
int init[10]={2,14,1,7,15,5,8,4,13,6};
for (int i = 0; i < 10; i++){
root = insertRbNode(root,init[i]);
}
rbNode* delNode = searchRBT(root,11);
printRBT(root,0);
deleteRbNode(delNode);
printf("after delete node 11\n");
printRBT(root,0);
return 0;
}
结果为
PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe
the 0th node : 7 with 1
the 1th node : 2 with 1
the 2th node : 1 with 1
the 2th node : 5 with 1
the 3th node : 4 with 0
the 3th node : 6 with 0
the 1th node : 11 with 1
the 2th node : 8 with 1
the 2th node : 14 with 1
the 3th node : 13 with 0
the 3th node : 15 with 0
after delete node 11
the 0th node : 7 with 1
the 1th node : 2 with 1
the 2th node : 1 with 1
the 2th node : 5 with 1
the 3th node : 4 with 0
the 3th node : 6 with 0
the 1th node : 8 with 1
the 2th node : 14 with 1
the 3th node : 13 with 0
the 3th node : 15 with 0
顺序统计树
顺序统计树是基于红黑树的一种数据扩张,除了红黑属性之外,其节点还包含子系个数的信息,即以当前节点为根的子树的所有节点个数。
在经历了实现红黑树的折磨之后,这种扩张显得轻而易举。首先在节点结构体中添加一个成员size
。然后修改插入操作,当插入新节点时,新节点的size
值为1,途中经历的所有指针指向的节点,其size
值都加1。
即
//rbNode* insertRbNode(rbNode *root, int val);
//...
while (temp = root->value < val
? root->right : root->left,temp!=NULL){
root->size += 1;
root = temp;
}
//...
删除操作时,记录最终被删除的节点指针,其所有父辈的size
均减一。
if (dNode->left == NULL && dNode->right == NULL){
if (dNode==pNode->right)
pNode->right = NULL;
else
pNode->left = NULL;
while(pNode.size-=1,pNode->father!=NULL){
pNode = pNode->father;
}
}
size
值一方面给出了当前节点子系的体量,另一方面也是对当前节点在所有节点中的大小排名的一个标记。当指针从根节点依次下沉时,顺带也继承了当前节点的区间信息,其实现为
rbNode* searchRBTN(rbNode *root, int n){
int low = 0; //左开右闭
int high = root->size;
if(n>high)
return NULL;
while (1){
//左子节点存在且size<n-low
if (root->left!=NULL && root->left->size<n-low){
root = root->left;
high = low + root->size;
}else{
root = root->right;
low = high - root->size;
}
if (root->right!=NULL && root->right->size==high-root->size)
return root;
if(root->left!=NULL && root->left->size == low+root->size-1)
return root;
}
}