標籤:
對於一種資料結構而言,遍曆是常見操作。二叉樹是一種基本的資料結構,是一種每個節點的兒子數目都不多於2的樹。二叉樹的節點聲明如下:
1 typedef struct TreeNode *PtrToNode;2 typedef struct TreeNode *BinTree;3 4 struct TreeNode5 {6 int Data; //為簡單起見,不妨假設樹節點的元素為int型7 BinTree Left;8 BinTree Right;9 };
二叉樹的遍曆主要有先序遍曆,中序遍曆,後序遍曆,層序遍曆四種方式,下面一一介紹。
1. 先序遍曆
在先序遍曆中,對節點的訪問工作是在它的左右兒子被訪問之前進行的。換言之,先序遍曆訪問節點的順序是根節點-左兒子-右兒子。由於樹可以通過遞迴來定義,所以樹的常見操作用遞迴實現常常是方便清晰的。遞迴實現的代碼如下:
1 void PreOrderTraversal(BinTree BT)2 {3 if( BT ) 4 {5 printf(“%d\n”, BT->Data); //對節點做些訪問比如列印 6 PreOrderTraversal(BT->Left); //訪問左兒子7 PreOrderTraversal(BT->Right); //訪問右兒子8 }9 }
由遞迴代碼可以看出,該遞迴為尾遞迴(尾遞迴即遞迴形式在函數末尾或者說在函數即將返回前)。尾遞迴的遞迴調用需要用棧儲存調用的資訊,當資料規模較大時容易越出棧空間。雖然現在大部分的編譯器能夠自動去除尾遞迴,但是即使如此,我們不妨自己去除。非遞迴先序遍曆演算法基本思路:使用堆棧
a. 遇到一個節點,訪問它,然後把它壓棧,並去遍曆它的左子樹;
b. 當左子樹遍曆結束後,從棧頂彈出該節點並將其指向右兒子,繼續a步驟;
c. 當所有節點訪問完即最後訪問的樹節點為空白且棧空時,停止。
實現代碼如下:
void PreOrderTraversal(BinTree BT){ BinTree T = BT; Stack S = CreatStack(MAX_SIZE); //建立並初始化堆棧S while(T || !IsEmpty(S)) { while(T) //一直向左並將沿途節點訪問(列印)後壓入堆棧 { printf("%d\n", T->Data); Push(S, T); T = T->Left; } if (!IsEmpty(S)) { T = Pop(S); //節點彈出堆棧 T = T->Right; //轉向右子樹 } }
}
2. 中序遍曆
中序遍曆的遍曆路徑與先序遍曆完全一樣。其實現的思路也與先序遍曆非常相似。其主要的不同點是訪問節點順序不同:中序遍曆是訪問完所有左兒子後再訪問根節點,最後訪問右兒子,即為左兒子-根節點-右兒子。
遞迴實現的代碼如下:
void InOrderTraversal(BinTree BT){ if(BT) { InOrderTraversal(BT->Left); printf("%d\n", BT->Data); InOrderTraversal(BT->Right); }}
非遞迴實現代碼如下:
void InOrderTraversal(BinTree BT){ BinTree T = BT; Stack S = CreatStack(MaxSize); //建立並初始化堆棧S while(T || !IsEmpty(S))
{ while(T) //一直向左並將沿途節點壓入堆棧 { Push(S,T); T = T->Left; } if(!IsEmpty(S)) { T = Pop(S); //節點彈出堆棧 printf("%d\n", T->Data); //(訪問) 列印結點 T = T->Right; //轉向右子樹 }
}}
3. 後序遍曆
後序遍曆與中序遍曆,先序遍曆的路徑也完全一樣。主要的不同點是後序遍曆訪問節點的順序是先訪問左兒子和右兒子,最後訪問節點,即左兒子-右兒子-根節點。
遞迴實現思路與中序遍曆和先序遍曆相似,代碼如下:
void PostOrderTraversal(BinTree BT){ if (BT) { PostOrderTraversal(BT->Left); PostOrderTraversal(BT->Right); printf("%d\n", BT->Data); }}
後序遍曆的非遞迴實現
思路一:
對於一個節點而言,要實現訪問順序為左兒子-右兒子-根節點,可以利用後進先出的棧,在節點不為空白的前提下,依次將根節點,右兒子,左兒子壓棧。故我們需要按照根節點-右兒子-左兒子的順序遍曆樹,而我們已經知道先序遍曆的順序是根節點-左兒子-右兒子,故只需將先序遍曆的左右調換並把訪問方式列印改為壓入另一個棧即可。最後一起列印棧中的元素。代碼如下:
void PostOrderTraversal(BinTree BT){ BinTree T = BT; Stack S1 = CreatStack(MAX_SIZE); //建立並初始化堆棧S1 Stack S2 = CreatStack(MAX_SIZE); //建立並初始化堆棧S2 while(T || !IsEmpty(S1)) { while(T) //一直向右並將沿途節點訪問(壓入S2)後壓入堆棧S1 { Push(S2, T); Push(S1, T); T = T->Right; } if (!IsEmpty(S1)) { T = Pop(S1); //節點彈出堆棧 T = T->Left; //轉向左子樹 } } while(!IsEmpty(S2)) //訪問(列印)S2中元素 { T = Pop(S2); printf("%d\n", T->Data); } }
思路一的優點是由於利用了先序遍曆的思想,代碼較簡潔,思路較清晰。缺點是需要用一個棧來儲存樹的所有節點,空間佔用較大。
思路二:
要訪問一個節點的條件上一個訪問的節點是右兒子。我們可以增加一個變數Prev來判斷當前節點Curr的上一個節點與它的關係來執行相應的操作。
- 若Prev為空白(Curr節點是根節點)或者Prev是Curr的父節點,將Curr節點的左孩子和右孩子分別壓入棧;
- 若Prev是Curr的左兒子,則將Curr的右兒子壓入棧;
- 否則Prev是Curr的右兒子,訪問Curr;
代碼如下:
void PostOrderTraversal(BinTree BT){ if(BT == NULL) return ; Stack S = CreatStack(MAX_SIZE); BinTree Prev = NULL , Curr = NULL; //初始化 s.push(BT); while(!IsEmpty(S)) { Curr = Top(S); //將棧頂元素賦給Curr if(Prev == NULL || Prev->Left == Curr || Prev->Right == Curr) //若Prev為NULL或是Curr的父節點 { if(Curr->Left != NULL) Push(S, Curr->Left); else if(Curr->Right != NULL) Push(S, Curr->Right); } else if(Curr->Left == Prev) //若Prev是Curr的左兒子 { if(Curr->Right != NULL) Push(S, Curr->Right); } else { printf("%d\n", Curr->Data); //訪問當前節點 Pop(S); //訪問後彈出 } Prev = Curr; //處理完當前節點後將Curr節點變為Prev節點 }}
4. 層序遍曆
二叉樹遍曆的核心問題是二維結構的線性化。我們通過節點訪問其左右兒子時,存在的問題是訪問左兒子後,右兒子怎麼訪問。因此我們需要一個儲存結構儲存暫時不訪問的節點。前面三種遍曆方式的非遞迴實現,我們是通過堆棧來儲存。事實上也可以通過隊列來儲存。
隊列實現的基本思路:遍曆從根節點開始,首先將根節點入隊,然後執行迴圈:節點出隊,訪問(訪問)根節點,將左兒子入隊,將右兒子入隊,直到隊列為空白停止。
這種遍曆方式的結果是將二叉樹從上到下,從左至右一層一層的遍曆,即層序遍曆,代碼實現如下:
void LevelOrderTraversal(BinTree BT){ BinTree T; Queue Q; //聲明一個隊列 if (BT == NULL) return; //如果樹為空白,直接返回 Q = CreatQueue(MAX_SIZE); //建立並初始化隊列 AddQ(Q, BT); //將根節點入隊 while (!IsEmpty(Q)) { T = DeleteQ(Q); //節點出隊 printf("%d\n", T->Data); //訪問出隊的節點 if (T->Left) AddQ(Q, T->Left); //若左兒子不為空白,將其入隊 if (T->Right) AddQ(Q, T->Right) //若右兒子不為空白,將其入隊 }}
理解上述四種二叉樹遍曆方式後,不妨來小試牛刀:List Leaves, Tree Traversals Again.
二叉樹的遍曆:先序中序後序遍曆的遞迴與非遞迴實現及層序遍曆