樹
《算法導論》中第一次引入樹這種數據結構是以二叉堆的形式出現的,並且在附錄中在圖的基礎上給出了樹在數學上的定義。如果從鏈表的觀點出發,那麼相當於是放寬了有序的要求,即允許兩個不同位置的元素有相等的序。
對於序爲的節點來說,可以指向多個序爲的節點,相應的後者稱爲前者的孩子,前者稱爲後者的父節點。其最大序即爲樹的高度。(下圖中數字表示的是節點的代數,而非值)
在上圖中,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;
}
}