實驗報告:二叉樹創建與遍歷
一、問題描述
二叉樹是一種實用范圍很廣的非線性結構,一棵非空二叉樹有也只有一個根結點,每個結點最多有兩個子樹,我們稱為左子樹與右子樹,當一個結點的左、右子樹都是空的時,沃恩稱此結點為葉子結點。
二叉樹有一些很好的性質,這里不再贅述。考慮如何存儲一棵樹,本實驗選擇使用鏈式存儲結構——二叉鏈表;如果事先知道需要存儲的二叉樹是滿二叉樹或者完全二叉樹,則可以考慮使用順序存儲,否則將浪費大量的存儲空間。
對於一棵既成的二叉樹,有三種遍歷方式——先序、中序與后序。可以證明,一棵形態固定的二叉樹與先序遍歷、后序遍歷生成的序列是1-1對應的,基於這一點,我們可以從先序序列或后序序列出發得到唯一與之對應的二叉樹。這是本實驗創建二叉樹的理論基礎。
值得注意的是,一個中序遍歷序列與二叉樹並不是1-1對應的關系,也就是說一個中序序列對應的二叉樹的形態並不固定,即使像本實驗中這樣添加了虛空結點也不是1-1對應的,所以中序列並不能夠生成一棵二叉樹。
本實驗主要給出先序創建二叉樹,以及中序遍歷該樹。
二、數據結構——二叉鏈表
二叉鏈表是二叉樹的鏈式存儲結構,其結構與二叉樹的性質有天然的契合。
我們知道,二叉樹的結點需要保存的信息有:當前結點的信息、左子樹、右子樹。基於如此的結構,我們在一個結點處分為數據域和指針域:數據域保存該結點自身的信息;指針域分為左、右指針域,分別指向該結點的左、右子樹;有必要時可以加上一個鏈域指向該結點的父親結點(雙親結點),當然這樣就成了三叉鏈表。三叉鏈表與二叉鏈表沒有太大的差別,只是三叉鏈表在具體操作涉及到尋找結點的祖先時會有優勢,比如尋找最近公共祖先(lca),這與本實驗無關,不再贅述。
值得注意的是,當結點的某個子樹不存在時,指向該結點的指針域應該是NULL的。有個小技巧是,可以人為定義一個“空指針域”,應該指向NULL的時候都指向這個所謂的“空結點”,可以一定程度上避免指針訪問越界的錯誤。
三、算法的設計和實現
這個算法的思想非常簡單。本實驗可以分為兩部分——創建二叉樹與遍歷二叉樹。
1、創建二叉樹(以先序序列為例)
樹的序列生成是一個遞歸的過程,所以以遍歷序列創建樹的時候也需要遞歸生成這棵樹。
以先序序列為例,如果當前標號非空,那么我們得到的就是當前結點的數據,保存該數據后,下一個數據——無論是虛空結點還是真實的數據——是左子樹的數據,所以我們將遞歸到左子樹中;如果當前標號是空的,即虛空結點,表示此處不存在結點,返回NULL即可;當前結點的左子樹遞歸返回后進入右子樹遞歸;返回該結點的地址,結束。
可以看出這就是一個模擬生成先序序列的過程,着眼於一個結點來說,就是先訪問當前子樹的根節點,然后左子樹,再右子樹,最后返回。
后序序列也可以類似地生成,具體細節將在下面提到。
2、遍歷二叉樹(以中序遍歷為例)
與樹的創建相似,二叉樹的遍歷同樣是一個遞歸的過程。
以中序序遍歷為例,如果左子樹非空,則遞歸進入左子樹;輸出當前結點的標號;如果右子樹非空,則遞歸進入右子樹;結束。如果不是需要輸出序列,而是想得到這個序列,那么很自然地可以想到棧,將輸出標號的操作改為將標號壓棧即可,別的操作不改動。
遍歷二叉樹的意義重大,比如中序遍歷一棵排序二叉樹,得到的就是保存的有序的序列。
四、預期結果和實驗中的問題
1、預期結果:
程序能夠根據輸入的先序序列(帶有虛空結點),正確地生成對應的二叉樹,並正確地輸出中序遍歷序列。下圖是一個生成三序的例子。

2、實驗中的問題及一些說明:
(1)如果沒有虛空結點,能否通過三序得到唯一對應的二叉樹?
答案是肯定的。下面將簡單地說明如何利用先序序列和中序序列得到對應的二叉樹。
A)分析:
對於一個先序序列來說,第一個結點一定是根結點,它的右邊是左子樹的先序序列,然后是右子樹的先序序列。這里的問題在於,無法找到左、右子樹序列的分界點,換句話說,僅靠一個先序序列我們無法得到對應的二叉樹。
這時我們再對中序序列的構成分析,我們可以在中序序列中找到根結點(根結點的標號已經在先序序列中得到了),它的左邊是左子樹的中序序列,右邊是右子樹的中序序列。
這時候我們發現,這里出現了一個子結構。因為一棵樹(子樹)的三序序列長度是相等的,所以我們可以得到左、右子樹的先序序列,從而對於任一子樹來說,我們得到了它的先序序列和中序序列,問題轉換成了相似的子問題。
B)實現:
a)先序序列第一個結點找到根結點;
b)在中序序列中找到該結點(此處可以順序查找,找到結束即可;當數據量比較大時推薦使用二分法查找),得到左、右子樹的結點數,從而分別得到左、右子樹對應的先序序列和中序序列;
c)分別遞歸進入左、右子樹,建樹。遞歸的邊界是葉子結點,它的先序和中序都是長度為1的自己的標號。
(2)包含虛空結點的后序序列如何生成對應的二叉樹?
有一個簡單的辦法,從后往前掃描后序序列,相當於是順序為“根結點-右子樹-左子樹”生成的序列。這個與先序序列生成對應二叉樹是一個鏡面的過程,不再贅述。
(3)中序序列能夠生成唯一對應的二叉樹嗎?
答案是否定的。中序序列與二叉樹並不是1-1對應的,下圖是一個例子:

這兩個都是合法的二叉樹,兩者的中序序列都是CBA,即使是加上了虛空結點,得到的中序序列也都是$C$B$A$($表示空格,也就是虛空結點),所以中序序列與二叉樹不是1-1對應的,當然不能通過中序序列生成一棵唯一的二叉樹。
(2)能否不使用遞歸操作?
這個問題可以歸結為研究遞歸操作的本質。
遞歸操作實際上是一個對棧的操作,以先序遍歷二叉樹為例:將當前結點壓棧;如果左子樹非空,將左子樹壓棧;如果右子樹非空,將右子樹壓棧;如果左、右子樹都是空的,即當前結點為葉子結點,彈棧;當結點的左右子樹都完成了操作,彈棧。
於是我們得到了一個有意思的結果:所有的遞歸算法是可以有非遞歸調用的實現方式的(這里的非遞歸調用實現方式指的是遞歸調用函數,其本質應該還是遞歸)。當然,也不排除一些遞歸解決的問題同樣可以用遞推解決,比如漢諾塔問題,這里不贅述。
附:c++源代碼:
1 /* 2 項目:創建二叉樹,三序遍歷 3 作者:張譯尹 4 */ 5 #include <iostream> 6 #include <cstdio> 7 8 using namespace std; 9 10 template <class T> class BiTree 11 { 12 private: 13 T data; //根結點的標志 14 BiTree *lch, *rch; //左右兒子 15 public: 16 void InitBiTree() //初始化二叉鏈表 17 { 18 data = 0; 19 lch = rch = NULL; 20 } 21 BiTree* CreatBiTree() //先序(帶虛空結點)遞歸建立二叉鏈表 22 { 23 BiTree <char> *rt = new BiTree; 24 rt -> InitBiTree(); 25 char ch; 26 scanf("%c", &ch); 27 if(ch != ' ') //非空指針,遞歸建樹 28 { 29 rt -> data = ch; 30 rt -> lch = CreatBiTree(); 31 rt -> rch = CreatBiTree(); 32 } 33 else 34 { 35 delete rt; 36 rt = NULL; 37 } 38 return rt; 39 } 40 void PreOrderTraverse() //遞歸先序遍歷 41 { 42 printf("%c", data); 43 if(lch) 44 lch -> PreOrderTraverse(); 45 if(rch) 46 rch -> PreOrderTraverse(); 47 } 48 void InOrderTraverse() //遞歸中序遍歷 49 { 50 if(lch) 51 lch -> InOrderTraverse(); 52 printf("%c", data); 53 if(rch) 54 rch -> InOrderTraverse(); 55 } 56 void PostOrderTraverse() //遞歸后序遍歷 57 { 58 if(lch) 59 lch -> PostOrderTraverse(); 60 if(rch) 61 rch -> PostOrderTraverse(); 62 printf("%c", data); 63 } 64 }; 65 66 int main() 67 { 68 BiTree <char> *Tree; 69 //Tree -> InitBiTree(); 70 71 printf("請輸入二叉樹的先序列,結點用字母表示,空域用空格表示。\n"); 72 Tree = Tree -> CreatBiTree(); 73 74 printf("先序序列:\n"); 75 Tree -> PreOrderTraverse(); 76 printf("\n"); 77 78 printf("中序序列:\n"); 79 Tree -> InOrderTraverse(); 80 printf("\n"); 81 82 printf("后序序列:\n"); 83 Tree -> PostOrderTraverse(); 84 printf("\n"); 85 86 return 0; 87 }
