徹底理解線索二叉樹
一、線索二叉樹的原理
通過考察各種二叉連結串列,不管兒叉樹的形態如何,空鏈域的個數總是多過非空鏈域的個數。準確的說,n各結點的二叉連結串列共有2n個鏈域,非空鏈域為n-1個,但其中的空鏈域卻有n+1個。如下圖所示。
因此,提出了一種方法,利用原來的空鏈域存放指標,指向樹中其他結點。這種指標稱為線索。
記ptr指向二叉連結串列中的一個結點,以下是建立線索的規則:
(1)如果ptr->lchild為空,則存放指向中序遍歷序列中該結點的前驅結點。這個結點稱為ptr的中序前驅;
(2)如果ptr->rchild為空,則存放指向中序遍歷序列中該結點的後繼結點。這個結點稱為ptr的中序後繼;
顯然,在決定lchild是指向左孩子還是前驅,rchild是指向右孩子還是後繼,需要一個區分標誌的。因此,我們在每個結點再增設兩個標誌域ltag和rtag,注意ltag和rtag只是區分0或1數字的布林型變數,其佔用記憶體空間要小於像lchild和rchild的指標變數。結點結構如下所示。
其中:
(1)ltag為0時指向該結點的左孩子,為1時指向該結點的前驅;
(2)rtag為0時指向該結點的右孩子,為1時指向該結點的後繼;
(3)因此對於上圖的二叉連結串列圖可以修改為下圖的養子。
二、線索二叉樹結構實現
二叉線索樹儲存結構定義如下:
/* 二叉樹的二叉線索儲存結構定義*/ typedef enum{Link, Thread}PointerTag; //Link = 0表示指向左右孩子指標;Thread = 1表示指向前驅或後繼的線索 typedef struct BitNode { char data; //結點資料 struct BitNode *lchild, *rchild; //左右孩子指標 PointerTag Ltag; //左右標誌 PointerTag rtal; }BitNode, *BiTree;
線索化的實質就是將二叉連結串列中的空指標改為指向前驅或後繼的線索。由於前驅和後繼資訊只有在遍歷該二叉樹時才能得到,所以,線索化的過程就是在遍歷的過程中修改空指標的過程。
中序遍歷線索化的遞迴函式程式碼如下:
BiTree pre; //全域性變數,始終指向剛剛訪問過的結點
//中序遍歷進行中序線索化
void InThreading(BiTree p)
{
if(p)
{
InThreading(p->lchild); //遞迴左子樹線索化
//===
if(!p->lchild) //沒有左孩子
{
p->ltag = Thread; //前驅線索
p->lchild = pre; //左孩子指標指向前驅
}
if(!pre->rchild) //沒有右孩子
{
pre->rtag = Thread; //後繼線索
pre->rchild = p; //前驅右孩子指標指向後繼(當前結點p)
}
pre = p;
//===
InThreading(p->rchild); //遞迴右子樹線索化
}
}
上述程式碼除了//===之間的程式碼以外,和二叉樹中序遍歷的遞迴程式碼機會完全一樣。只不過將列印結點的功能改成了線索化的功能。
中間部分程式碼做了這樣的事情:
因為此時p結點的後繼還沒有訪問到,因此只能對它的前驅結點pre的右指標rchild做判斷,if(!pre->rchild)表示如果為空,則p就是pre的後繼,於是pre->rchild = p,並且設定pre->rtag = Thread,完成後繼結點的線索化。如圖:
if(!p->lchild)表示如果某結點的左指標域為空,因為其前驅結點剛剛訪問過,賦值了pre,所以可以將pre賦值給p->lchild,並修改p->ltag = Thread(也就是定義為1)以完成前驅結點的線索化。
完成前驅和後繼的判斷後,不要忘記當前結點p賦值給pre,以便於下一次使用。
有了線索二叉樹後,對它進行遍歷時,其實就等於操作一個雙向連結串列結構。
和雙向連結串列結點一樣,在二叉樹連結串列上新增一個頭結點,如下圖所示,並令其lchild域的指標指向二叉樹的根結點(圖中第一步),其rchild域的指標指向中序遍歷訪問時的最後一個結點(圖中第二步)。反之,令二叉樹的中序序列中第一個結點中,lchild域指標和最後一個結點的rchild域指標均指向頭結點(圖中第三和第四步)。這樣的好處是:我們既可以從第一個結點起順後繼進行遍歷,也可以從最後一個結點起順前驅進行遍歷。
遍歷程式碼如下所示。
//t指向頭結點,頭結點左鏈lchild指向根結點,頭結點右鏈rchild指向中序遍歷的最後一個結點。
//中序遍歷二叉線索樹表示二叉樹t
int InOrderThraverse_Thr(BiTree t)
{
BiTree p;
p = t->lchild; //p指向根結點
while(p != t) //空樹或遍歷結束時p == t
{
while(p->ltag == Link) //當ltag = 0時迴圈到中序序列的第一個結點
{
p = p->lchild;
}
printf("%c ", p->data); //顯示結點資料,可以更改為其他對結點的操作
while(p->rtag == Thread && p->rchild != t)
{
p = p->rchild;
printf("%c ", p->data);
}
p = p->rchild; //p進入其右子樹
}
return OK;
}
說明:
(1)程式碼中,p = t->lchild;意思就是上圖中的第一步,讓p指向根結點開始遍歷;
(2)while(p != t)其實意思就是迴圈直到圖中的第四步出現,此時意味著p指向了頭結點,於是與t相等(t是指向頭結點的指標),結束迴圈,否則一直迴圈下去進行遍歷操作;
(3)while(p-ltag == Link)這個迴圈,就是由A->B->D->H,此時H結點的ltag不是link(就是不等於0),所以結束此迴圈;
(4)然後就是列印H;
(5)while(p->rtag == Thread && p->rchild != t),由於結點H的rtag = Thread(就是等於1),且不是指向頭結點。因此列印H的後繼D,之後因為D的rtag是Link,因此退出迴圈;
(6)p=p->rchild;意味著p指向了結點D的右孩子I;
(7).....,就這樣不斷的迴圈遍歷,直到打印出HDIBJEAFCG,結束遍歷操作。
從這段程式碼可以看出,它等於是一個連結串列的掃描,所以時間複雜度為O(n)。
由於充分利用了空指標域的空間(等於節省了空間),又保證了建立時的一次遍歷就可以終生受用後繼的資訊(意味著節省了時間)。所以在實際問題中,如果所用的二叉樹需要經過遍歷或查詢結點時需要某種遍歷序列中的前驅和後繼,那麼採用線索二叉連結串列的儲存結構就是非常不錯的選擇。
#include <stdio.h>
#include <stdlib.h>
#define ERROR 0
#define OK 1
typedef enum{Link, Thread} PointerTag; //link = 0表示指向左右孩子指標
//Thread = 1表示指向前驅或後繼的線索
typedef struct BitNode
{
char data; //結點資料
struct BitNode *lchild; //左右孩子指標
struct BitNode *rchild;
PointerTag ltag; //左右標誌
PointerTag rtag;
}BitNode, *BiTree;
BiTree pre; //全域性變數,始終指向剛剛訪問過的結點
//前序建立二叉樹
void CreateTree(BiTree *t)
{
char ch;
scanf("%c", &ch);
if(ch == '#')
{
*t = NULL;
}
else
{
(*t) = (BiTree)malloc(sizeof(BitNode));
if((*t) == NULL)
{
return;
}
(*t)->data = ch;
CreateTree(&((*t)->lchild));
CreateTree(&((*t)->rchild));
}
}
//t指向頭結點,頭結點左鏈lchild指向根結點,頭結點右鏈rchild指向中序遍歷的最後一個結點。
//中序遍歷二叉線索樹表示的二叉樹t
int InOrderThraverse_Thr(BiTree t)
{
BiTree p;
p = t->lchild; //p指向根結點
while(p != t)
{
while(p->ltag == Link) //當ltag = 0時迴圈到中序序列的第一個結點
{
p = p->lchild;
}
printf("%c ", p->data); //顯示結點資料,可以更改為其他對結點的操作
while(p->rtag == Thread && p->rchild != t)
{
p = p->rchild;
printf("%c ", p->data);
}
p = p->rchild; //p進入其右子樹
}
return OK;
}
//中序遍歷進行中序線索化
void InThreading(BiTree p)
{
if(p)
{
InThreading(p->lchild); //遞迴左子樹線索化
if(!p->lchild) //沒有左孩子
{
p->ltag = Thread; //前驅線索
p->lchild = pre; //左孩子指標指向前驅,這裡是第3步
}
if(!pre->rchild) //沒有右孩子
{
pre->rtag = Thread; //後繼線索
pre->rchild = p; //前驅右孩子指標指向後繼(當前結點p)
}
pre = p;
InThreading(p->rchild); //遞迴右子樹線索化
}
}
//建立頭結點,中序線索二叉樹
int InOrderThread_Head(BiTree *h, BiTree t)
{
(*h) = (BiTree)malloc(sizeof(BitNode));
if((*h) == NULL)
{
return ERROR;
}
(*h)->rchild = *h;
(*h)->rtag = Link;
if(!t) //如果為NULL
{
(*h)->lchild = *h;
(*h)->ltag = Link;
}
else
{
pre = *h;
(*h)->lchild = t; //第一步
(*h)->ltag = Link;
InThreading(t); //找到最後一個結點
pre->rchild = *h; //第四步
pre->rtag = Thread;
(*h)->rchild = pre; //第二步
}
}
int main(int argc, char **argv)
{
BiTree t;
BiTree temp;
printf("請輸入前序二叉樹的內容:\n");
CreateTree(&t); //建立二叉樹
InOrderThread_Head(&temp, t); //加入頭結點,併線索化
printf("輸出中序二叉樹的內容:\n");
InOrderThraverse_Thr(temp);
printf("\n");
return 0;
}