12.遍曆二叉樹與二叉樹的建立,12遍曆二叉樹
一、遍曆二叉樹1.定義 二叉樹的遍曆(travering binary tree)是指從根結點出發,按照某種次序依次訪問二叉樹中的所有結點,使得每個結點被訪問一次且僅被訪問一次。2.前序走訪(1)規則:若二叉樹為空白,則空操作返回。否則,先訪問根結點,然後前序走訪左子樹,再前序走訪右子樹。(2)執行個體
前序走訪結果為:A BDGH CEIF分析:當最先訪問根結點後,然後前序走訪左子樹。當訪問根的左子樹時,這裡"前序走訪"即我們將B假設為左子樹的根來遍曆。(3)演算法 從二叉樹定義可知,其是用遞迴的方式,所以,實現遍曆演算法也可以採用遞迴。
<span style="font-size:18px;">/*二叉樹的前序走訪遞迴演算法*/void PreOrderTraverse(BiTree T){ if(T==NULL) //若樹為空白,返回為空白 return; printf("%c",T->data); //顯示結點資料,可以更改為其他對結點操作 PreOrderTraverse(T->lchild); //再先遍曆左子樹 PreOrderTraverse(T->rchild); //最後遍曆右子樹}</span>
執行個體分析: 如所示,當調用PreOrderTraverse(T)函數時,程式運行過程如下:a)調用PreOrderTraverse(T),T根結點不為null,所以執行printf,列印字母A;b)然後,調用PreOrderTraverse(T->lchild),訪問A結點的左孩子,B結點不為null,執行printf列印出B;c)此時再次遞迴調用PreOrderTraverse(T->lchild),訪問了B結點的左孩子,執行printf列印字母D;d)再次執行PreOrderTraverse(T->lchild),訪問D結點的左孩子,執行printf列印字母G;e)再次執行PreOrderTraverse(T->lchild),訪問G結點的左孩子,由於G結點沒有左孩子,則返回為空白,所以T==null,返回此函數,此時調用PreOrderTraverse(T->rchild),訪問D結點的右孩子,執行printf列印字母H;f).......依次繼續列印後面字母即可。3.中序遍曆
(1)規則:若樹為空白,則空操作返回。否則,從根結點開始(注意並不是先訪問根結點),中序遍曆根結點的左子樹,然後是訪問根結點,最後中序遍曆右子樹。
(2)執行個體
中序遍曆結果為:GDHB A EICF
(3)演算法
<span style="font-size:18px;">/*二叉樹的中序遍曆遞迴演算法*/void InOrderTraverse(BiTree T){ if(T == NULL) return; InOrderTraverse(T->lchild); //中序遍曆左子樹 printf("%c",T->data); //顯示結點資料,可以更改為其他對結點操作 InOrderTraverse(T->rchild); //最後中序遍曆右子樹}</span>
4.後序遍曆
(1)規則:若樹為空白,則空操作返回。否則,從左至右先葉子後結點的方式遍曆訪問左右子樹,最後是訪問根結點。
(2)執行個體
後序遍曆結果為:GHDB IEFC A
(3)演算法
<span style="font-size:18px;">/*二叉樹的後序遍曆遞迴演算法*/void PostOrderTraverse(BiTree T){ if(T==NULL) return; PostOrderTraverse(T->lchild);//先後序遍曆左子樹 PostOrderTraverse(T->rchild);//再後序遍曆右子樹 printf("%c",T->data); //顯示結點資料,可以更改為其他對結點操作 }</span>
5.層序遍曆
(1)規則:若樹為空白,則空操作返回。否則,從樹的第一層,也就是根結點開始訪問,從上而下逐層遍曆,在同一層中,按從左至右的順序對結點逐個訪問。
(2)執行個體
層序遍曆結果為:A BC DEF GHI
二、推導遍曆結果 二叉樹遍曆性質: (1)已經前序走訪序列和中序遍曆序列,可以唯一確定一顆二叉樹; (2)已經後序遍曆序列和中序遍曆序列,可以唯一確定一顆二叉樹;1.假設已經一顆二叉樹的前序走訪序列為ABCDEF,中序遍曆序列為CBAEDF,請問這顆二叉樹的後序遍曆結果是多少?分析:2.假設一顆二叉樹的中西序列是ABCDEFG,後序序列是BDCAFGE,求前序序列?分析:
三、二叉樹的建立 對於一顆普通的二叉樹,我們需將二叉樹中每個借點的null 指標引出一個虛結點,其值為一特定值,比如"#"。我們稱這種處理後的二叉樹為原二叉樹的擴充二叉樹,擴充二叉樹可以通過一個"前序"或"中序"或"後序"遍曆序列確定一顆二叉樹。(1)擴充二叉樹的前序遍序列為:AB#D##C##(2)實現演算法
<span style="font-size:18px;">/*按前序輸入而二叉樹中借點的值(一個字元) * 其中,#表示空樹,構造二叉鏈表表示二叉樹T */void CreateBitree(Bitree *T){ TElemType ch; scanf("%c",&ch); //輸入結點資料字元 if(ch=='#') *T=NULL; else { *T=(BiTree)malloc(sizeof(BiTNode)); //為資料為字元的結點在記憶體中分配空間 if(!*T) //如果分配未成功則異常結束(記憶體溢出) exit(OVERFLOW); (*T)->data = ch; //產生根結點 CreateBiTree(&(*T)->lchild); //構造左子樹 CreateBiiTree(&(*T)->rchild); //構造右子樹 }}</span>
總結:實際上,建立二叉樹也是利用了遞迴的遠離,只不過在原來應該是列印結點的地方改成了產生結點、給結點賦值操作而已。另外,我們也可以通過中序或後序遍曆的方式實現二叉樹的建立,只不過代碼裡產生的結點和構造左右字子樹的代碼順序交換一下即可。
四、線索二叉樹 對於一個有n個結點的二叉鏈表,每個結點有指向左右孩子的兩個指標域,所以一共是2n個指標域。而n個結點的二叉樹一共有n-1條分支線(根結點無前驅),也就是說,其實存在2n-(n-1)=n+1個null 指標域。由於這些空間不儲存任何事物,這樣會導致記憶體資源的浪費。另外,在二叉鏈表上,我們只能知道每個結點指向其左右孩子結點的地址,而不知道某個結點的前驅是誰,後繼是誰。想要知道,就必須遍曆一次鏈表,以後每次需要知道時,都必須先遍曆一次。為了提供記憶體空間的利用率和節省操作時間,我們可以考慮在建立就明確結點的前驅和後繼。
1.線索二叉樹 如果將指向前驅和後驅的指標稱為線索,那麼加上線索的二叉鏈表則稱為線索鏈表;加上線索的二叉樹就稱之為線索二叉樹(Threaded Binary Tree),對二叉樹以某種次序遍曆使其變為線索二叉樹的過程稱作是線索化。通過線索二叉樹,我們對它進行遍曆就等於操作一個雙向鏈表結構,從而大大提高了訪問速度。 如二叉樹按中序遍曆後:HDIBJE A FCG,null 指標指(結點rchild指標或lchild指標)向的後繼或前驅。
2.線索二叉樹結點結構與實現
(1)結點結構 由於無法知道某一結點的lchild是指向它的左孩子還是指向前驅,rchild是指向右孩子還是指向後繼。因此,我們在每個結點再增設兩個標誌域ltag和rtag,需要注意的是ltag和rtag只是存放0或1數位布爾型變數,其佔用的記憶體空間要小於像lchild和rchild的指標變數。結點結構如下:
(2)線索二叉樹結構實現
<span style="font-size:18px;">/*二叉樹的二叉線索儲存結構定義* Link==0表示左右孩子指標* Thread==1表示指向前驅或後驅的線索*/typedef eum {Link,Thread} PointerTag;typedef struct BiThrNode /*二叉線索儲存結點結構*/{ TElemType data; //資料域:結點資料 struct BiThrNode *lchild,*rchild; //指標域:左右孩子指標 PointerTag LTag; PointerTag RTag; //左右標誌 }BiThrNode,*BiThrTree;</span>
3.中序遍曆線索化的遞迴函式(痛點) 線索化的實質就是將二叉鏈表中的null 指標改為指向前驅或後繼的線索。由於前驅和後繼的資訊只有在遍曆該二叉樹時才能得到,所以線索化的過程就是在遍曆的過程中修改null 指標的過程。 中序遍曆線索化的遞迴函式代碼如下: BiThrTree pre; //全域變數,始終指向剛剛訪問過的結點 /*中序遍曆進行中序線索化*/ void InThreading(BitThrTree p) { if(p) { InThreading(p->lchild); //遞迴左子樹線索化 if(!p->lchild) //結點無左孩子 { p->LTag=Thread; //前驅線索:將結點左指標標誌置1,說明左指標指向該結點的前驅 p->lchild=pre; //左孩子指標指向前驅 } if(!pre->rchild) //前驅沒有右孩子 { pre->RTag=Thread; //後繼線索 pre-rchild=p; //前驅右孩子指標指向後繼(當前結點p) } pre=p; //保持pre指向p的前驅 InThreading(p->rchild); //遞迴右子樹線索化 } }源碼分析:(1)結點前驅線索化 if(!p->lchild)表示如果某結點的左指標域為空白,因為其前驅節點剛剛訪問過,賦值給了pre,所以可以將pre賦值給p->lchild,並修改p->LTag=Thread(也就是定義為1)以完成前驅結點的線索化。(2)結點後驅線索化 由於該節點還沒有訪問到,因此只能對它的前驅結點pre的右指標rchild做判斷,if(!pre->rchild)表示如果為空白,則p就是pre的後繼,於是pre->rchild=p,並且設定pre->RTag=Thread,完成後繼結點的線索化。(3) pre=p語句的作用是完成前驅和後繼的判斷後,將當前的結點p賦值給pre,以便下一次使用。
4.中序遍曆線索二叉樹T的非遞迴演算法 二叉樹的二叉線索儲存表示(以中序為例):線上索鏈表上添加一個頭結點,並令其lchild域的指標指向二叉樹的根結點,其rchild域的指標指向中序遍曆時訪問的最後一個結點。令二叉樹中序串列中的第一個結點的lchild域指標和最後一個結點的rchild域的指標均指向頭結點,這樣就建立了一個雙向線索鏈表。這樣定義的好處是既可以從第一個結點起順後繼進行遍曆,也可以從最後一個結點起順前驅進行遍曆。
/*T指向頭結點,頭結點左鍵lchild指向根結點,頭結點右鏈rchild指向中序遍曆的最後一個結點* 中序遍曆二叉線索鏈表表示的二叉樹T,時間複雜度為O(n)*/Status InOrderTraverse_Thr(BiThTree T){ BiThrTree 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進至其右子樹根 }}源碼分析:(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),所以結束此迴圈並列印H;(4)while(p->RTag == Thread && p->rchild !=T),由於結點H的RTag==Thread(就是等於1),且不是指向頭結點。因此列印H的後繼D,之後因為D的RTag是Link,因此退出迴圈;(5)p=p->rchild,即p指向了結點D的右孩子。 .....................不斷迴圈,直到列印出HDIBJEAFCG結束遍曆操作。
總結:二叉樹的線索化有利於節省空間的和時間,在實際問題中,如果所用的二叉樹需經常遍曆或尋找結點時需要某種遍曆序列中的前驅和後繼,那麼採用線索二叉鏈表的儲存結構是一個非常不錯的選擇。