參考書籍:數據結構(C語言版)嚴蔚敏吳偉民編著清華大學出版社
本文中的代碼可從這裏下載:https://github.com/qingyujean/data-structure
1.什麼是線索二叉樹
空的左孩子指針指向該結點的前驅;空的右孩子指針指向該結點的後繼。這種附加的指針值稱爲線索,帶線索的二叉樹稱爲線索二叉樹。
在不同的遍歷次序下,二叉樹中的每個結點一般有不同的前驅和後繼。因此,線索二叉樹又分爲前序線索二叉樹、中序線索二叉樹和後序線索二叉樹3種。
根據二叉樹的特性,n個結點的二叉樹,採用鏈式存儲結構時,有n+1個空鏈域,可以利用這些空鏈域存放指向結點的直接前驅和直接後繼結點的指針。爲此做如下規定:當結點的左指針爲空(即無左子樹)時,令該指針指向按某種方式遍歷二叉樹時得到的該結點的前驅結點;當結點的右指針爲空(即無右子樹)時,令該指針指向按某種方式遍歷二叉樹時得到的該結點的後繼結點;爲了避免混淆,還需要增加兩個標誌位來區分指針指向的是其孩子還是前驅及後繼。
2.建立線索二叉樹
線索化的過程就是遍歷二叉樹的過程。在遍歷的過程中,檢查當前結點的左、右指針域是否爲空,若爲空,將它們改爲指向前驅結點或後繼結點的線索。
當線索花二叉樹以後,遍歷二叉樹的時間複雜度雖然仍然是O(n),但常數因子要比上一篇博文中的算法小,且不需要設棧。因此如果程序中所用的二叉樹需要經常遍歷或查找結點在遍歷所得的線性序列的前驅和後繼,則應採用線索鏈表作爲二叉樹的存儲結構。
線索二叉樹上的查找:例如中序線索化二叉樹以後,結點的後繼要麼右標誌爲1,後繼直接是右線索所指的結點,要麼是遍歷其右子樹時訪問的第一個結點,即右子樹中最左下的結點;而結點的前驅要麼左標誌爲1,前驅就是左線索所指的結點,要麼是遍歷其左子樹時訪問的最後一個結點,即左子樹中最右下的結點。
爲方便起見。仿照線性表的存儲結構,在二叉樹的線索鏈表上也添加一個頭結點,並令其lchild域的指針指向二叉樹的根結點,其rchild域的指針指向中序遍歷時訪問的最後一個結點;反之,令二叉樹的中序序列中的第一個結點的lchild域指針和最後一個節點的rchild域的指針均指向頭結點。這好比爲二叉樹建立了一個雙向線索鏈表,既可以從第一個結點起順後繼進行遍歷,也可以從最後一個結點起,順前驅進行遍歷。
3.代碼實現
下面以中序線索化二叉樹爲例進行實現。
3.1線索二叉樹的定義
#include<stdio.h>
#include<stdlib.h>
//#define Link 0//指針標誌
//#define Thread 1//線索標誌
typedef char TElemType;
//中序線索二叉樹
typedef enum PointerTag {Link, Thread};//結點的child域類型,link表示是指針,指向孩子結點,thread表示是線索,指示前驅或後繼結點
//補充:枚舉型是一個集合,集合中的元素(枚舉成員)是一些命名的整型常量,元素之間用逗號隔開
//第一個枚舉成員的默認值爲整型的0,後續枚舉成員的值在前一個成員上加1。
//可以人爲設定枚舉成員的值,從而自定義某個範圍內的整數。
typedef struct ThrBiNode{
TElemType data;
ThrBiNode *lchild, *rchild;//左右孩子指針
PointerTag lTag, rTag;//左右標誌
}ThrBiNode, *ThrBiTree;
3.2中序線索化二叉樹
線索劃的過程即爲在遍歷的過程中修改空指針的過程,爲記下遍歷過程中訪問結點的先後順序,附設指針pre總指向當前訪問結點p的前驅結點。
//中序遍歷二叉樹T,並將其 中序線索化,Thrt指向頭結點
void inOrderThreading(ThrBiTree T, ThrBiTree &Thrt){
//初始化線索鏈表,爲其建立一個頭結點
Thrt = (ThrBiTree)malloc(sizeof(ThrBiNode));
Thrt->lTag = Link;
Thrt->rTag = Thread;
//Thrt->rchild = Thrt;//右指針回指,因爲若T爲空樹的話,只會指向下面的if語句,那麼Thrt->rchild
if(!T){//如果二叉樹爲空樹,則Thrt->lchild指針回指
Thrt->lchild = Thrt;
Thrt->rchild = Thrt;//右指針回指
}else{
Thrt->lchild = T;
ThrBiNode *pre = Thrt;//pre指針總指向當前結點的前驅結點
inThreading(T, pre);
//繼續爲最後一個結點加入線索
//此時pre應指向最後一個結點
pre->rTag = Thread;
pre->rchild = Thrt;//最後一個結點的rchild域指針回指
Thrt->rchild = pre;//頭結點的rchild域指針指向最後一個結點
}
}
//中序遍歷進行中序線索化(左、根、右)
void inThreading(ThrBiTree T, ThrBiTree &pre){
if(T){
inThreading(T->lchild, pre);//左子樹線索化
if(!T->lchild){//當前結點的左孩子爲空
T->lTag = Thread;
T->lchild = pre;
}else{
T->lTag = Link;
}
if(!pre->rchild){//前驅結點的右孩子爲空
pre->rTag = Thread;
pre->rchild = T;
}else{
pre->rTag = Link;
}
pre = T;
inThreading(T->rchild, pre);//右子樹線索化
}
}
3.3中序遍歷線索二叉樹
//T指向頭結點,頭結點的lchild鏈域指針指向二叉樹的根結點
//中序遍歷打印二叉線索樹T(非遞歸算法)
void inOrderTraversePrint(ThrBiTree T){
ThrBiNode *p = T->lchild;//p指向根結點
while(p != T){//空樹或遍歷結束時,p == T
while(p->lTag == Link){
p = p->lchild;
}
//此時p指向中序遍歷序列的第一個結點(最左下的結點)
printf("%c ", p->data);//打印(訪問)其左子樹爲空的結點
while(p->rTag == Thread && p->rchild != T){
p = p->rchild;
printf("%c ", p->data);//訪問後繼結點
}
//當p所指結點的rchild指向的是孩子結點而不是線索時,p的後繼應該是其右子樹的最左下的結點,即遍歷其右子樹時訪問的第一個節點
p = p->rchild;
}
printf("\n");
}
3.4創建二叉樹
//利用先序序列建立一顆二叉樹,'.'代表空樹
//測試用例1:abc..de.g..f...#
//測試用例2:-+a..*b..-c..d../e..f..#
void createBiTreeByPreOrder(ThrBiTree &T){
//按先序次序輸入二叉樹中節點的值(一個字符),點號字符表示空樹,構造二叉鏈表表示的二叉樹
//注意:若輸入的字符數(不含#號)爲n個,則相應的空樹即點號就應該有n+1個
char ch;
scanf("%c", &ch);
if(ch != '#'){
if(ch == '.'){
T = NULL;
}else{
T = (ThrBiNode *)malloc(sizeof(ThrBiNode));
T->data = ch;
createBiTreeByPreOrder(T->lchild);
createBiTreeByPreOrder(T->rchild);
}
}
}
3.5演示
測試用例
二叉樹:
測試輸入:-+a..*b..-c..d../e..f..#
對應的中序線索二叉樹:
void main(){
ThrBiTree T;
printf("請按先序次序輸入二叉樹各節點的值,以空格表示空樹,以#號結束:\n");
createBiTreeByPreOrder(T);//建立二叉樹
ThrBiTree Thrt;
inOrderThreading(T, Thrt);//將二叉樹T中序線索化
inOrderTraversePrint(Thrt);//中序遍歷二叉線索樹(通過線索鏈表,就像訪問線性表一樣)
//當然之前的不按線索,直接根據二叉樹的結構進行遍歷依然可以(層次、先序、後序、中序)
}