二叉樹遍歷算法


首先,個人認為,二叉樹是很能體會遞歸算法思想的,因為二叉樹的結構是leftTree->root<-rightTree,對於每個非葉子節點,該規律都適用,因此關於二叉樹的很多算法也都能用遞歸思想搞定。遞歸的優點在於代碼簡潔,但效率卻是問題。其次,對於各種順序的遍歷,又有着相應的非遞歸算法,非遞歸算法的代碼量相對大一點,但更容易掌控,而且效率更優。

先看節點結構:

1 struct Bitree{
2   int val;
3   Bitree *left, *right;
4   Bitree(int x):val(x), left(nullptr), right(nullptr){
5   };
6 };

 

1. 中序遍歷

  •  遞歸算法

顯然,中序遍歷的順序為leftTree, root, rightTree,顯然先遍歷左子樹,然后是根,最后右子樹。中序遍歷的遞歸算法自然也就出來了。

 1 void inOrderTraverse1(Bitree *root){
 3   if(root){
 5     inOrderTraverse1(root->left);
 7     visit(root);
 9     inOrderTraverse1(root->right);
11   }
13 }
  • 非遞歸算法1.

非遞歸算法的思想也比較簡單,按從左到右進行訪問。先指針往左探尋到底,然后觀察最后一個非空節點是否有右節點,若有,將該右節點作為新的探尋起點,再進行下一輪的探尋。顯然,“一探到底”的思路需要使用stack來幫助緩存之前的節點。

 1 void inOrderTraverse2(Bitree *root){
 2   stack<Bitree *> S;
 3   S.push(root);
 4   Bitree *p = root;
 5   while(!S.empty()){
 6     p = S.top();
 7     while(p){
 8       S.push(p->left);
 9       p = p->left;
10     }
11     S.pop();//pop out the nullptr
12     if(!S.empty()){
13       p = S.top();
14       visit(p);
15       S.pop();
16       S.push(p->right);//push its right child into the stack
17     }
18   }
19 }
  • 非遞歸算法2
 1 void inOrderTraverse3(Bitree *root){
 2   stack<Bitree *> S;
 3   Bitree *p = root;
 4   while(p || !S.empty()){
 5     if(p){
 6       S.push(p);
 7       p = p->left;
 8     }else{
 9       p = S.top();
10       visit(p);
11       S.pop();
12       p = p->right;
13     }
14   }
15 }

個人認為,雖然兩種非遞歸算法的思路完全一樣,但非遞歸算法2比非遞歸算法1代碼要更為簡潔,更值得推薦。

 

2. 前序遍歷

  • 遞歸算法

前序遍歷的順序為root,leftTree,rightTree,直接上代碼

1 void preOrderTraverse1(Bitree *root){
2   if(root){
3     visit(root);
4     preOrderTraverse1(root->left);
5     preOrderTraverse1(root->right);
6   }
7 }
  • 非遞歸算法1

對於前序遍歷的非遞歸算法,和中序遍歷的非遞歸算法非常相似,不過是在進棧時就訪問該節點,而不是之后再訪問。由於代碼相似,先給出一種

 1 void preOrderTraverse2(Bitree *root){
 2   stack<Bitree *> S;
 3   Bitree *p = root;
 4   while(p || !S.empty()){
 5     if(p){
 6       visit(p);
 7       S.push(p);
 8       p = p->left;
 9     }else{
10       p = S.top();
11       S.pop();
12       p = p->right;
13     }
14   }
15 }
  •  非遞歸算法2

該算法采用了和前序遍歷相同的思想,即root節點先進棧,root節點出棧時,將其右節點先進棧,然后是左節點進棧。這樣,利用棧先進后出的性質,訪問順序自然變為了root,左子樹,右子樹。

 1 void preOrderTraverse3(Bitree *root){
 2   if(!root){
 3     return;
 4   }
 5   stack<Bitree *> S;
 6   Bitree *p = root;
 7   S.push(root);
 8   while(!S.empty()){
 9     p = S.top();
10     visit(p);
11     S.pop();
12     if(p->right){
13       S.push(p->right);
14     }
15     if(p->left){
16       S.push(p->left);
17     }
18   }
19 }

 

3. 后續遍歷

  • 遞歸算法

后續遍歷的順序是leftTree,rightTree和root,因此遞歸算法也自然出來了

1 void postOrderTraverse1(Bitree *root){
2   if(root){
3     postOrderTraverse1(root->left);
4     postOrderTraverse1(root->right);
5     visit(root);
6   }
7 }
  • 非遞歸算法1

和之前中序和前序算法不同,后續遍歷的root節點要最后才能被訪問,因此,我們若想訪問某節點,那么我們需要知道該節點的右節點是否已經被訪問過。只有該節點的右節點為null,或者已被訪問過,那么該節點才能被訪問;否則需要先將右節點訪問完。為了判斷該節點的右節點是否已經被訪問過,需另外設一個記錄指針last來指示已經訪問過的節點,如果之前訪問過的節點last恰為該節點的右節點,說明其右子樹已經訪問完,應該訪問該節點。

 1 void postOrderTraverse2(Bitree *root){
 2   Bitree *last = nullptr;
 3   Bitree *p = root;
 4   stack<Bitree *> S;
 5   while(p || !S.empty()){
 6     while(p){
 7       S.push(p);
 8       p = p->left;
 9     }
10     p = S.top();
11     if(p->right && p->right != last){
12       p = p->right;
13     }else{
14       visit(p);
15       S.pop();
16       last = p;
17       p = nullptr;//p needs to be updated to null for next loop
18     }
19   }
20 }

tip 1:后續遍歷中,root節點最后才能被訪問到,因此,棧能記錄每一個節點的路徑,包括葉子節點。這一點性質可用於求解和樹的路徑有關的問題。

  •  非遞歸算法2

和前序遍歷的非遞歸算法2一樣,這里也給出后續遍歷對應的非遞歸算法2,思路也是類似。由於后序遍歷中,根節點要最后才能被訪問到,不像前序遍歷中剛訪問到便可以輸出。但在實際查找過程中,我們又只能先從根節點開始查找,才能接着查找左子樹和右子樹,由此可以再利用棧先進后出的特性來存儲根節點。

 1 void postOrderTraverse3(Bitree *root){
 2   if(!root){
 3     return;
 4   }
 5   Bitree *p = root;
 6   stack<Bitree *> S;
 7   stack<Bitree *> postOrder;
 8   S.push(p);
 9   while(!S.empty()){
10     p = S.top();
11     postOrder.push(p);
12     S.pop();
13     if(p->left){
14       S.push(p->left);//first IN, later OUT
15     }
16     if(p->right){
17       S.push(p->right);//later IN, first OUT
18     }
19   }
20   while(!postOrder.empty()){
21     p = postOrder.top();
22     visit(p);
23     postOrder.pop();
24   }
25 }

 

4. 層序遍歷

層序遍歷的思想和之前三種遍歷方式不同,需要借助queue來對節點進行緩存,先進隊列的節點需要先離開。這和圖的BFS思想一樣,畢竟樹本質上也是一種特殊的圖。

 1 void levelOrderTraverse(Bitree *root){
 2   if(!root){
 3     return;
 4   }
 5   queue<Bitree *> Q;
 6   Bitree *p = nullptr;
 7   Q.push(root);
 8   while(!Q.empty()){
 9     p = Q.front();
10     Q.pop();
11     visit(p);
12     if(p->left){
13       Q.push(p->left);
14     }
15     if(p->right){
16       Q.push(p->right);
17     }
18   }
19 }

Tip 2: 層序遍歷思想簡單,利用queue來一層一層輸出。因此,可用於求解數的寬度和高度。


免責聲明!

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



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