我們在上一章中,學習了二叉樹的數據結構。因為二叉樹的特殊性,它不同於普通的樹,所以可以使用順序存儲結構來存儲。但是,用順序存儲結構會存在浪費空間的弊端。之后,我們學習了二叉鏈表。用鏈式存儲結構存儲樹,結點結構為一個數據域data,兩個指針域lchild、rchild。樹的數據結構講完了,但是沒有講怎樣生成一棵二叉樹。今天,我們來學習怎樣生成一棵二叉樹。
1.生成二叉樹:
要想生成一棵二叉樹,我們需要清楚結點是否有左孩子、右孩子結點。為了搞清楚這一點,我們需要對二叉樹進行擴展,經過擴展后的二叉樹,稱為擴展二叉樹。
我們以前序順序來生成上圖的二叉樹,前序順序為AB#D##C##.
/*按前序輸入二叉樹中結點的值(一個字符)*/ /*#表示空樹*/ CreateBirTree(BirTree T) { TElemType ch; //結點值類型 Scanf("%c",&ch); //接收輸入參數 if(ch=="#") //判斷是否是空樹 *T==Null // 樹為空 else { *T=(BirTree) malloc(sizeof(BiTNode)); //內存中開辟空間,存放結點 if(!*T) exit(OVERFLOW); (*T)->data=ch; /*生成根結點*/ CreateBiTree(&(*T)->lchild); /*構造左子樹*/ CreateBiTree(&(*T)->rchild);/*構造右子樹*/ } }
上面就是我們的前序創建二叉樹的算法代碼,我們同樣可以使用后序、中序創建二叉樹。算法代碼,與上面大致一樣,只是調換一下遞歸順序。
2.線索二叉樹:
數據結構之所以重要,是因為好的數據結構可以使計算機運作更高效。好的數據結構,應該是運行時間短、占用空間少。二叉鏈表中,有很多空的指針域。這些沒用的指針域在內存中占有一定的空間,這是一種浪費。如下圖:
上圖的中序遍歷順序為:HDIBJEAFCG,通過中序遍歷順序,我們知道I的前驅是D,后繼是B,F的前驅是A,后繼是C。但是,這是在我們以中序遍歷整棵樹之后得到的。每次,需要找某個結點的前驅、后繼,我們就需要遍歷整棵樹,這是時間上的浪費。
通過上面的空間、時間兩方面的考慮,我們需要利用那些沒用的空指針域。我們可以在創建二叉樹時,利用這些空的指針域存放結點的前驅、后繼。這樣的話,只需一次遍歷就可以知道遍歷次序。如下圖:
我們利用lchild存放前驅結點、rchild存放后繼結點。這樣我們就可以將二叉鏈表的缺點彌補。lchild、rchild指向前驅、后繼結點,我們稱這樣的指針為線索,加上線索的二叉樹,稱為線索二叉樹。我們以二叉樹某種次序遍歷使其變為線索二叉樹的過程稱做是線索化。
但是問題又來了,我們怎么會知道結點的lchild、rchild是空的。所以,我們需要增添兩個記錄指針域狀態的變量。
當ltag=0、rtag=0時,指針指向左子樹、右子樹。
當ltag=1、rtag=1時,指針指向前驅、后繼。
整理后的線索二叉圖。
3.線索二叉樹結構實現:
線索二叉樹結點結構:
/*二叉樹的二叉線索存儲結構定義*/ typedef enum {Link,Thread} pointerTag;/*Link==0表示指向左右孩子指針.Thread=1,表示指向前驅、后繼*/ typedef struct BiThrNode /*二叉線索存儲結點結構*/ { TElemType data; /*結點數據*/ struct BiThrNode *lchild、*rchild; /*左右孩子指針*/ pointerTag Ltag; pointerTag Rtag; /*左右標志*/ }BiThrNode,*BiThrTree;
其實線索化的過程,就是在遍歷二叉樹時,將空指針指向前驅、后繼的過程。
我們看一下中序遍歷線索化的過程函數代碼:
BiThrTree pre; /*全局變量,始終指向剛剛訪問過的結點*/ /*中序遍歷進行中序線索化*/ void InThreading(BiThrTree p) { if(p) //判斷樹是否為空 { InThreading(p->lchild); /*遞歸左子樹線索化*/ if!(p->lchild) /*判斷左孩子結點是否為空*/ { p->Ltag=Thread; /*指針域存放前驅*/ p->lchild=pre; /*將pre賦給p的前驅指針*/ } if(!pre->rchild) /*判斷剛剛訪問過的結點是否有右孩子*/ { pre->Rtag=Thread; /*指針域存放后繼*/ pre->rchild=p; } pre=p; /*將p賦給Pre*/ InThreading(p->rchild); /*遞歸右子樹線索化*/ } }
pre,始終指向剛剛訪問過的結點。函數首先,判斷p是否為空。不為空,遞歸左子樹。判斷結點是否有左孩子,也就是判斷結點的lchild指針域是否為空。當lchild指針域不為空,將標識設置為Thread,表明此結點的lchild指向前驅。將pre賦給p的lchild。因為后繼元素,還沒有遍歷到。所以只能對Pre進行后繼賦值。最后將p賦值給pre,再遞歸右子樹線索化。我們可以發現,中序線索化的函數,與中序遍歷函數很相似。只是把輸出操作,改為向指針域賦值。我沒呢對線索二叉樹操作,其實就是對一個雙向鏈表操作。
我們看一下遍歷線索二叉樹的函數:
/*中序遍歷二叉鏈表*/ /*T指向頭結點,頭結點的右鍵rchild指向中序遍歷的最后一個結點*/ Status InOrderTraverse_Thr(BiTree T) { BiThrTree p; p=T;/*p指向根結點*/ while(p!=T) /*判斷p是否為空樹、是否遍歷完整棵樹*/ { 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; } return ok; }
這就是我們今天講的內容了。






