遍歷二叉樹能夠用遞歸的方法去實現,也能夠用非遞歸的方法去實現。遞歸代碼的好處是簡潔,直觀,最主要的仍是遞歸的代碼少,很快就能夠寫完。但咱們知道,遞歸的調用會用到一個專門的棧,這個棧的深度是有限的,若是遞歸函數調用的次數不少,超過棧限制的深度,那麼程序就會崩潰。這個時候就須要把遞歸的代碼改成非遞歸了。所以,瞭解掌握遍歷二叉樹的非遞歸實現仍是頗有必要的。算法
下面會給出先序遍歷,中序遍歷,後序遍歷的遞歸與非遞歸代碼,以及層次遍歷的代碼。數據結構
首先,先給出二叉樹的節點定義:ide
1 struct BinTNode { 2 int data; 3 BinTNode *lchild, *rchild; 4 };
先序遍歷,就是先訪問根節點,再訪問左子樹,最後再訪問右子樹。而要訪問左子樹,一樣是先訪問左子樹的根節點,再訪問左子樹的左子樹,最後再訪問左子樹的右子樹。訪問右子樹也是一樣的方法。因此,咱們天然而然想到用遞歸的算法。函數
爲了方便表述,咱們遍歷二叉樹所作的事情是把該節點的值輸出。post
對於一個遞歸函數,咱們不該該跳到遞歸裏面去,而是去理解遞歸函數的定義,也就是它的做用是什麼?遞歸結束後會返回什麼樣的結果?spa
咱們把先序遍歷的遞歸函數定義爲:傳入一個根節點,若是這個節點不爲空,就輸出根節點的值,而後把左子樹的全部節點的值輸出,再把右子樹的全部節點的值輸出,這就是先序遍歷遞歸函數的做用。因爲函數的返回值是void,全部遞歸結束後不會有返回結果。因此再按照先序遍歷的定義,咱們能夠把遞歸函數寫成下面這樣:.net
1 void preOrderTraversal(BinTNode *T) { 2 if (T) { // 節點不爲空才能夠輸出值 3 cout << T -> data; // 先輸出根節點的值 4 preOrderTraversal(T -> lchild); // 再把左子樹的根,也就是T -> lchild傳到咱們的遞歸函數中,輸出左子樹全部節點的值 5 preOrderTraversal(T -> rchild); // 最後把右子樹的根,也就是T -> rchild傳到咱們的遞歸函數中,輸出右子樹全部節點的值 6 } 7 }
瞭解了遞歸的代碼後,接下來就是先序遍歷的非遞歸實現。指針
咱們知道,當調用遞歸函數來遍歷二叉樹,每個節點都會被訪問3次。code
而先序遍歷就對應着當該節點被第1次訪問時就,輸出該節點的值。因爲遞歸的本質是運用棧,所以咱們也能夠模擬一個棧來實現非遞歸。當遇到一個不爲空的節點時,咱們把這個節點壓入棧,這就對應於第1次訪問這個節點,因此在壓入棧後,輸出該節點的值。而後一直作T = T -> lchild這個動做,把節點對應的左子樹節點壓到棧,同時輸出節點的值。直到左子樹爲空,這時就彈出棧頂元素,這個時候該節點被第2次訪問。把彈出節點的右子樹節點再壓入棧中。這個過程不斷重複,直到節點和棧都爲空。這就實現了先序遍歷,先是根節點,再是左子樹,最後是右子樹。blog
先序遍歷的非遞歸代碼以下:
1 void preOrderTraversal(BinTNode *T) { 2 SNode *S = initStack(); // 申請一個棧 3 while (T || !isEmpty(S)) { // 循環的條件是根節點和棧不一樣時爲空 4 if (T) { // 若是根節點存在不爲空 5 push(S, T); // 把根節點壓入棧 6 cout << T -> data; // 因爲是第一次訪問該節點,因此輸出節點的值 7 T = T -> lchild; // 把左子樹的根節點賦值給T,進入下一次循環 8 } 9 else { // 若是根節點爲空 10 T = pop(S); // 彈出棧頂元素,第二次訪問該節點 11 T = T -> rchild; // 把右子樹的根節點賦值給T,進入下一次循環 12 } 13 } 14 }
還有另一種先序遍歷的非遞歸代碼,和上面的代碼幾乎同樣:
1 void preOrderTraversal(BinTNode *T) { 2 SNode *S = initStack(); 3 while (T || !isEmpty(S)) { 4 while (T) { 5 push(S, T); 6 cout << T -> data; 7 T = T -> lchild; 8 } 9 if (!isEmpty(S)) { 10 T = pop(S); 11 T = T -> rchild; 12 } 13 } 14 }
中序遍歷,就是先訪問左子樹,再訪問根節點,最後再訪問右子樹。而要訪問左子樹,一樣是先訪問左子樹的左子樹,再訪問左子樹的根節點,最後再訪問左子樹的右子樹。訪問右子樹也是一樣的方法。因此,一樣能夠用遞歸去實現。
咱們把中序遍歷的遞歸函數定義爲:傳入一個根節點,若是這個節點不爲空,先把左子樹的全部節點的值輸出,再輸出根節點的值,最後把右子樹的全部節點的值輸出。其實,按照中序遍歷的定義,把先序遍歷的部分遞歸代碼進行交換,就變成中序遍歷了:
1 void inOrderTraversal(BinTNode *T) { 2 if (T) { // 節點不爲空才能夠輸出值 3 inOrderTraversal(T -> lchild); // 先把左子樹的根,也就是T -> lchild傳到咱們的遞歸函數中,輸出左子樹全部節點的值 4 cout << T -> data; // 再輸出根節點的值 5 inOrderTraversal(T -> rchild); // 最後把右子樹的根,也就是T -> rchild傳到咱們的遞歸函數中,輸出右子樹全部節點的值 6 } 7 }
接下來是中序遍歷的非遞歸實現。按中序遍歷的定義,當節點被第2次訪問時,咱們就輸出節點的值。因此中序遍歷和先序遍歷的非遞歸實現幾乎同樣,只不過是在節點被第2次訪問時才輸出該節點的值,因此咱們只須要把輸出語句改放到該節點被第2次訪問以後就能夠了,也就是改放到節點從棧頂被彈出以後。
1 void inOrderTraversal(BinTNode *T) { 2 SNode *S = initStack(); 3 while (T || !isEmpty(S)) { 4 if (T) { 5 push(S, T); // 把根節點壓入棧,第一次訪問該節點 6 T = T -> lchild; 7 } 8 else { 9 T = pop(S); // 彈出棧頂元素,第二次訪問該節點 10 cout << T -> data; // 因爲是第二次訪問該節點,因此輸出節點的值 11 T = T -> rchild; 12 } 13 } 14 }
還有另一種中序遍歷的非遞歸代碼,和上面的代碼幾乎同樣:
1 void inOrderTraversal(BinTNode *T) { 2 SNode *S = initStack(); 3 while (T || !isEmpty(S)) { 4 while (T) { 5 push(S, T); 6 T = T -> lchild; 7 } 8 if (!isEmpty(S)) { 9 T = pop(S); 10 cout << T -> data; 11 T = T -> rchild; 12 } 13 } 14 }
後序遍歷,就是先訪問左子樹,再訪問右子樹,最後再訪問根節點。而要訪問左子樹,一樣是先訪問左子樹的左子樹,再訪問左子樹的右子樹,最後再訪問左子樹的根節點。訪問右子樹也是一樣的方法。因此,一樣能夠用遞歸去實現。
咱們把後序遍歷的遞歸函數定義爲:傳入一個根節點,若是這個節點不爲空,先把左子樹的全部節點的值輸出,再把右子樹的全部節點的值輸出,最後再輸出根節點的值。和上面同樣,後序遍歷的遞歸函數只須要把部分遞歸代碼進行交換:
1 void postOrderTraversal(BinTNode *T) { 2 if (T) { // 節點不爲空才能夠輸出值 3 postOrderTraversal(T -> lchild); // 先把左子樹的根,也就是T -> lchild傳到咱們的遞歸函數中,輸出左子樹全部節點的值 4 postOrderTraversal(T -> rchild); // 再把右子樹的根,也就是T -> rchild傳到咱們的遞歸函數中,輸出右子樹全部節點的值 5 cout << T -> data; // 最後輸出根節點的值 6 } 7 }
至於後序遍歷的非遞歸實現,就沒有那麼容易了。若是咱們嘗試在前面的先序遍歷和中序遍歷的非遞歸代碼中,調換cout << T -> data; 這條語句的位置,咱們會發現不管咱們把它放在哪裏,都沒法實現後續遍歷。這是因爲在先序遍歷和中序遍歷的非遞歸代碼中,每一個節點最多能被訪問2次,也就是在壓入和彈出時被訪問。而後序遍歷要求是在節點被第3次訪問時才輸出節點的值。因此很明顯,以前的非遞歸函數並不可以實現後序遍歷。因此咱們只可以用其餘的方法來實現非遞歸的後序遍歷。
下面給出兩種不一樣的後序遍歷的非遞歸代碼實現:
1. 在節點中加入一個標誌域。
1 struct BinTNode { 2 int data; 3 BinTNode *lchild, *rchild; 4 bool isFirst; // 第一次訪問節點時賦值爲true;第二次訪問時,也就是從棧頂彈出時賦值爲false,再壓入棧中;當節點再彈出時已經是第三次訪問該節點了 5 };
標誌域的做用就是,當節點是第一次被彈出時,若是節點的標誌域爲true,那麼咱們再次把它壓入棧裏面,同時把標誌域改成false,這樣該節點就能夠再彈出一次。當再次彈出該節點時,又由於此時節點的標誌域爲false,不會再被壓入,從而該節點就能夠實現被訪問3次了。
這樣子咱們就能夠對一個節點訪問3次,在第3次訪問時輸出該節點的值,從而就能夠實現後序遍歷了:
1 void postOrderTraversal(BinTNode *T) { 2 SNode *S = initStack(); 3 while (T || !isEmpty(S)) { 4 if (T) { 5 push(S, T); // 把根節點壓入棧,第一次訪問該節點 6 T -> isFirst = true; // 標誌域賦值爲true 7 T = T -> lchild; 8 } 9 else { 10 T = pop(S); 11 if (T -> isFirst) { // 若是節點的標誌域爲true 12 push(S, T); // 咱們繼續把它壓入棧中,同時該節點被第二次訪問 13 T -> isFirst = false; // 同時再爲該節點的標誌域賦值爲false,下一次再彈出該節點時就再也不壓入棧中 14 T = T -> rchild; 15 } 16 else { // 若是節點的標誌域爲false 17 cout << T -> data; // 此時是第三次訪問該節點,能夠輸出該節點的值了 18 T = NULL; // 該節點的左右孩子都訪問完了,咱們把NULL賦值給T,在下一次的循環,去接收棧頂元素 19 } 20 } 21 } 22 }
2. 藉助輔助指針last,last指向最近訪問過的節點,也就是指向從棧頂彈出後,沒有再被壓入棧的那個節點。
用棧來存儲節點時,按照先序遍歷和中序遍歷的非遞歸代碼,節點只能被訪問兩次。然後序遍歷的順序是先訪問左子樹,再訪問右子樹,最後才訪問根節點。因此咱們應該分清除當一個根節點從棧頂彈出時,上一次從棧頂彈出的節點究竟是它的左子樹的根節點,還從它右子樹的根節點。因此,能夠用輔助指針last,來指向最近訪問過的節點,看它是否是該節點右子樹的根節點。若是是,就說明該節點的左右子樹都已經訪問完了,能夠輸出該節點的值了。
舉個簡單的例子:
1 void postOrderTraversal(BinTNode *T) { 2 SNode *S = initStack(); 3 BinTNode *last = NULL; // last指向剛訪問完的節點 4 while (T || !isEmpty(S)) { 5 if (T) { 6 push(S, T); 7 T = T -> left; 8 } 9 else { 10 T = pop(S); 11 if (T -> rchild && T -> rchild != last) { // 若是該根節點的右孩子不爲空,而且該根節點的右孩子不是剛訪問的那個節點(這意味着該根節點的右子樹尚未訪問,是從左子樹返回到該根節點的) 12 push(S, T); // 再把該根節點壓入,這樣子當該節點再次被彈出時,已經是被第三次訪問了 13 T = T -> rchild; // 向下一次循環傳入該根節點右子樹的根節點 14 } 15 else { // 根節點的右孩子爲空或者該根節點的右孩子就是剛訪問的那個節點(這意味着該根節點的右子樹已經被訪問了,是從右子樹返回到該根節點的) 16 cout << T -> data; // 第三次訪問該節點,因此輸出節點的值 17 last = T; // 由於該根節點被訪問了,因此last指向該節點 18 T = NULL; // 因爲該節點的左右孩子都訪問完了,咱們把NULL賦值給T,在下一次的循環,去接收棧頂元素 19 } 20 } 21 } 22 }
最後一個是層次遍歷,它不是用棧來實現的,而是用隊列來實現的。相似於圖的廣度優先搜索(BFS)。
所以,若是要用遞歸來實現層次遍歷,這會是很困難的事情。這裏就不討論了。
下面給出層次遍歷的代碼:
1 void levelOrderTraversal(BinTNode *T) { 2 if (T == NULL) return; 3 4 QNode *Q = initQueue(); 5 push(Q, T); 6 while (!isEmpty(Q)) { 7 T = pop(S); 8 cout << T -> data; 9 10 if (T -> lchild) push(Q, T -> lchild); 11 if (T -> rchild) push(Q, T -> rchild); 12 } 13 }
《數據結構:C語言版-第二版》
浙江大學——數據結構:https://www.icourse163.org/course/ZJU-93001?tid=1461682474
二叉樹後序遍歷的非遞歸實現:https://blog.csdn.net/u013161323/article/details/53925313