遍歷二叉樹的遞歸與非遞歸代碼實現


  遍歷二叉樹可以用遞歸的方法去實現,也可以用非遞歸的方法去實現。遞歸代碼的好處是簡潔,直觀,最主要的還是遞歸的代碼少,很快就可以寫完。但我們知道,遞歸的調用會用到一個專門的棧,這個棧的深度是有限的,如果遞歸函數調用的次數很多,超過棧限制的深度,那么程序就會崩潰。這個時候就需要把遞歸的代碼改為非遞歸了。因此,了解掌握遍歷二叉樹的非遞歸實現還是很有必要的。

  下面會給出先序遍歷中序遍歷后序遍歷的遞歸與非遞歸代碼,以及層次遍歷的代碼。

  首先,先給出二叉樹的節點定義:

1 struct BinTNode {
2     int data;
3     BinTNode *lchild, *rchild;
4 };

 

先序遍歷

  先序遍歷,就是先訪問根節點,再訪問左子樹,最后再訪問右子樹。而要訪問左子樹,同樣是先訪問左子樹的根節點,再訪問左子樹的左子樹,最后再訪問左子樹的右子樹。訪問右子樹也是同樣的方法。所以,我們自然而然想到用遞歸的算法。

  為了方便表述,我們遍歷二叉樹所做的事情是把該節點的值輸出。

  對於一個遞歸函數,我們不應該跳到遞歸里面去,而是去理解遞歸函數的定義,也就是它的作用是什么?遞歸結束后會返回什么樣的結果?

  我們把先序遍歷的遞歸函數定義為:傳入一個根節點,如果這個節點不為空,就輸出根節點的值,然后把左子樹的所有節點的值輸出,再把右子樹的所有節點的值輸出,這就是先序遍歷遞歸函數的作用。由於函數的返回值是void,所有遞歸結束后不會有返回結果。所以再按照先序遍歷的定義,我們可以把遞歸函數寫成下面這樣:

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次

  而先序遍歷就對應着當該節點被第1次訪問時就,輸出該節點的值。由於遞歸的本質是運用棧,因此我們也可以模擬一個棧來實現非遞歸。當遇到一個不為空的節點時,我們把這個節點壓入棧,這就對應於第1次訪問這個節點,所以在壓入棧后,輸出該節點的值。然后一直做T = T -> lchild這個動作,把節點對應的左子樹節點壓到棧,同時輸出節點的值。直到左子樹為空,這時就彈出棧頂元素,這個時候該節點被第2次訪問。把彈出節點的右子樹節點再壓入棧中。這個過程不斷重復,直到節點和棧都為空。這就實現了先序遍歷,先是根節點,再是左子樹,最后是右子樹。

  先序遍歷的非遞歸代碼如下:

 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 }
preOrderTraversal

 

中序遍歷

  中序遍歷,就是先訪問左子樹,再訪問根節點,最后再訪問右子樹。而要訪問左子樹,同樣是先訪問左子樹的左子樹,再訪問左子樹的根節點,最后再訪問左子樹的右子樹。訪問右子樹也是同樣的方法。所以,同樣可以用遞歸去實現。

  我們把中序遍歷的遞歸函數定義為:傳入一個根節點,如果這個節點不為空,先把左子樹的所有節點的值輸出,再輸出根節點的值,最后把右子樹的所有節點的值輸出。其實,按照中序遍歷的定義,把先序遍歷的部分遞歸代碼進行交換,就變成中序遍歷了:

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 }
inOrderTraversal

 

后序遍歷

  后序遍歷,就是先訪問左子樹,再訪問右子樹,最后再訪問根節點。而要訪問左子樹,同樣是先訪問左子樹的左子樹,再訪問左子樹的右子樹,最后再訪問左子樹的根節點。訪問右子樹也是同樣的方法。所以,同樣可以用遞歸去實現。

  我們把后序遍歷的遞歸函數定義為:傳入一個根節點,如果這個節點不為空,先把左子樹的所有節點的值輸出,再把右子樹的所有節點的值輸出,最后再輸出根節點的值。和上面一樣,后序遍歷的遞歸函數只需要把部分遞歸代碼進行交換:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM