首先,個人認為,二叉樹是很能體會遞歸算法思想的,因為二叉樹的結構是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來一層一層輸出。因此,可用於求解數的寬度和高度。
