二叉樹不少算法題都與其遍歷相關,筆者通過大量學習、思考,整理總結寫下二叉樹的遍歷篇,涵蓋遞歸和非遞歸實現。node
#include <stdio.h> #include <iostream> #include <stack> using namespace std; struct BTNode { int value; struct BTNode *left, *right; BTNode(int value_) :value(value_),left(NULL),right(NULL){}; }; //訪問函數:可根據實際進行修改 void visit(BTNode* node) { cout << node->value << " "; }
/** * 先序遍歷二叉樹 */ void PreOrder(BTNode* root) { if (root) { visit(root); PreOrder(root->left); PreOrder(root->right); } } /** * 中序遍歷二叉樹 */ void InOrder(BTNode* root) { if (root) { InOrder(root->left); visit(root); InOrder(root->right); } } /** * 後序遍歷二叉樹 */ void PostOrder(BTNode* root) { if (root) { PostOrder(root->left); PostOrder(root->right); visit(root); } }
算法過程:ios
由先序遍歷過程可知,先序遍歷的開始節點是根節點,而後用指針p 指向當前要處理的節點,沿着二叉樹的左下方逐一訪問,並將它們一一進棧,直至棧頂節點爲最左下節點,指針p爲空。此時面臨兩種狀況。(1)對棧頂節點的右子樹訪問(若是有的話,且未被訪問),對右子樹進行一樣的處理;(2)若無右子樹,則用指針last記錄最後一個訪問的節點,指向棧頂節點,彈棧。如此重複操做,直至棧空爲止。算法
void PreOrder_1a(BTNode *root) { if (NULL == root) return; stack<BTNode*> stack; BTNode *p = root; BTNode *last = NULL; do { while (p) { visit(p); stack.push(p); p = p->left; } //此時p = NULL,棧頂節點爲左子樹最左節點 if (!stack.empty()) { BTNode *t = stack.top(); if (t->right != NULL && t->right != last) { p = t->right; } else {//若無右子樹,則指針P仍爲空,則不斷彈棧(沿着雙親方向)尋找有未被訪問的右子樹的節點 last = t; stack.pop(); } } } while (!stack.empty()); }
算法過程:此算法與PreOrder_1a有殊途同歸之處,巧妙之處在於對上述兩種狀況的處理。指針p記錄當前棧頂結點的前一個已訪問的結點。若無右子樹、或者右子樹已被訪問,則用指針p記錄當前棧頂節點,彈棧,不斷沿着雙親方向尋找有未訪問右子樹的節點,找到即退出循環,不然直至棧空。當棧頂節點的右孩子是p時,則將cur指向右孩子,設置flag =0 ,退出當前搜索循環(不斷彈棧,搜索有右節點且未被訪問的祖先節點),而後對右子樹進行一樣的處理。如此反覆操做,直至棧空爲止。數據結構
void PreOrder_1b(BTNode *root) { if (NULL == root) return; stack<BTNode*>stack; int flag; BTNode *cur = root,*p; do{ while (cur) { visit(cur); stack.push(cur); cur = cur->left; } //執行到此處時,棧頂元素沒有左孩子或左子樹均已訪問過 p = NULL; //p指向棧頂結點的前一個已訪問的結點 flag = 1; //表示*cur的左孩子已訪問或者爲空 while (!stack.empty() && flag == 1) { cur = stack.top(); if (cur->right == p) //表示右孩子結點爲空或者已經訪問完右孩子結點 { stack.pop(); p = cur; //p指向剛訪問過的結點 } else { cur = cur->right; //cur指向右孩子結點 flag = 0; //設置未被訪問的標記 } } } while (!stack.empty()); }
PreOrder_2a 和 PreOrder_2b 算法思路大致相同,PreOrder_2a 實現比較簡潔函數
算法過程:post
用指針p指向當前要處理的節點,沿着左下方向逐一訪問並壓棧,直至指針P爲空,棧頂節點爲最左下節點。而後p指向棧頂節點的右節點(無論是否空);若右節點爲空,則繼續彈棧。若右節點非空,則按上述一樣處理右節點。如此重複操做直至棧空爲止。學習
缺陷:PreOrder_2 當訪問棧頂節點的右節點時,會丟失當前棧頂節點信息,致使從根節點到當前棧頂節點的右節點路徑不完整。spa
優勢:算法思路清晰易懂,邏輯簡單。指針
void PreOrder_2a(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode*> stack; while (p || !stack.empty()) { if (p) { visit(p); stack.push(p); p = p->left; } else { BTNode *top = stack.top(); p = top->right; stack.pop(); } } } void PreOrder_2b(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode*> stack; while (!stack.empty() ||p) { while (p) { visit(p); stack.push(p); p = p->left; } if (!stack.empty()) { BTNode *top = stack.top(); p = top->right; stack.pop(); } } }
算法過程:code
用指針p指向當前要處理的節點。先把根節點壓棧,棧非空時進入循環,出棧棧頂節點並訪問,而後按照先序遍歷先左後右的逆過程把當前節點的右節點壓棧(若是有的話),再把左節點壓棧(若是有的話)。如此重複操做,直至棧空爲止。
特色:棧頂節點保存的是先序遍歷下一個要訪問的節點,棧保存的全部節點不是根到要訪問節點的路徑。
void PreOrder_3(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode *> stack; stack.push(p); while (!stack.empty()) { p = stack.top(); stack.pop(); visit(p); if (p->right) stack.push(p->right); if (p->left) stack.push(p->left); } }
中序遍歷非遞歸算法要把握訪問棧頂節點的時機。
算法過程:
用指針cur指向當前要處理的節點。先掃描(並不是訪問)根節點的全部左節點並將它們一一進棧,直至棧頂節點爲最左下節點,指針cur爲空。
此時主要分兩種狀況。(1)棧頂節點的左節點爲空或者左節點已訪問,訪問棧頂節點(訪問位置很重要!),如有右節點則將cur指向右節點,退出當前while循環,對右節點進行上述一樣的操做,若無右節點則用指針p記錄站頂節點並彈棧;(2)站頂節點的右節點爲空或已訪問,用指針p記錄站頂節點並彈棧。
內部第二個while循環可稱爲訪問搜索循環,在棧頂節點的左節點爲空或者左節點已訪問的狀況下,訪問棧頂節點,如有右節點,則退出循環,不然不斷彈棧。
如此重複操做,直至棧空爲止。
void InOrder_1(BTNode *root) { if (NULL == root) return; stack<BTNode *>stack; BTNode *cur = root,*p = NULL; int flag = 1; do{ while (cur){ stack.push(cur); cur = cur->left; } //執行到此處時,棧頂元素沒有左孩子或左子樹均已訪問過 p = NULL; //p指向棧頂結點的前一個已訪問的結點 flag = 1; //表示*cur的左孩子已訪問或者爲空 while (!stack.empty() && flag) { cur = stack.top(); if (cur->left == p) //左節點爲空 或者左節點已訪問 { visit(cur); //訪問當前棧頂節點 if (cur->right) //如有右節點 當前節點指向右節點,並退出當前循環,進入上面的壓棧循環 { cur = cur->right; flag = 0; //flag = 0 標記右節點的左子樹未訪問 } else //當前節點沒有右節點,P記錄訪問完的當前節點,彈棧 { p = cur; stack.pop(); } } else // 此時 cur->right == P 即訪問完右子樹 ,P記錄訪問完的當前節點,彈棧 { p = cur; stack.pop(); } } } while (!stack.empty()); }
算法過程:
用指針p指向當前要處理的節點。先掃描(並不是訪問)根節點的全部左節點並將它們一一進棧,當無左節點時表示棧頂節點無左子樹,而後出棧這個節點,並訪問它,將p指向剛出棧節點的右孩子,對右孩子進行一樣的處理。如此重複操做,直至棧空爲止。
須要注意的是:當節點*p的全部左下節點入棧後,這時的棧頂節點要麼沒有左子樹,要麼其左子樹已訪問,就能夠訪問棧頂節點了!
InOrder_2a 、 InOrder_2b 與 PreOrder_1a 、PreOrder_1b 代碼基本相同,惟一不一樣的是訪問節點的時機,把握好可方便理解和記憶。
void InOrder_2a(BTNode *root) { if (NULL == root) return; BTNode *p = root; stack<BTNode *>stack; while (p || !stack.empty()) { while (p) { stack.push(p); p = p->left; } if (!stack.empty()) { p = stack.top(); visit(p); stack.pop(); } } } void InOrder_2b(BTNode *root) { if (NULL == root) return; stack<BTNode *>stack; BTNode *p = root; while (p || !stack.empty()) { while (p) { stack.push(p); p = p->left; } if (!stack.empty()) { p = stack.top(); visit(p); p = p->right; stack.pop(); } } }
void PostOrder_1(BTNode *root) { if (NULL == root) return; stack<BTNode*>stack; int flag; BTNode *cur = root, *p; do{ while (cur) { stack.push(cur); cur = cur->left; } //執行到此處時,棧頂元素沒有左孩子或左子樹均已訪問過 p = NULL; //p指向棧頂結點的前一個已訪問的結點 flag = 1; //表示*cur的左孩子已訪問或者爲空 while (!stack.empty() && flag == 1) { cur = stack.top(); if (cur->right == p) {//表示右孩子結點爲空,或者已經訪問cur的右子樹(p一定是後序遍歷cur的右子樹最後一個訪問節點) visit(cur); p = cur; //p指向剛訪問過的結點 stack.pop(); } else { cur = cur->right; //cur指向右孩子結點 flag = 0; //表示*cur的左孩子還沒有訪問過 } } } while (!stack.empty()); }
算法過程:
先把根節點壓棧,用指針p記錄上一個被訪問的節點。在棧爲空時進入循環,取出棧頂節點。
此時有兩種狀況:
(1)訪問當前節點,注意把握訪問的時機,若是當前節點是葉子節點,訪問當前節點;若是上一個訪問的節點是當前節點的左節點(說明無右節點),訪問當前節點;若是上一個訪問的節點是當前節點的右節點(說明左右節點都有),訪問當前節點;指針p記錄當前訪問的節點,彈棧。 因此,只有當前節點是葉子節點,或者上一個訪問的節點是當前節點的左節點(無右) 或右節點(左右都有) ,才能夠訪問當前節點。
(2)壓棧。 後序遍歷順序爲 左、右、根,按照逆序,先把右壓棧,再把左壓棧(若是有的話)。
如此重複操做,直至棧空爲止。
void PostOrder_2(BTNode* root) { if (NULL == root) return; BTNode *cur = root, *p=NULL; stack<BTNode*> stack; stack.push(root); while (!stack.empty()) { cur = stack.top(); if (cur->left == NULL && cur->right == NULL || (p!= NULL && (cur->left==p || cur->right == p))) { visit(cur); stack.pop(); p = cur; } else { if (cur->right) stack.push(cur->right); if (cur->left) stack.push(cur->left); } } }
二叉樹的廣度優先遍歷,就是層次遍歷,藉助隊列實現
在進行層次遍歷時,對某一層的節點訪問完後,再按照對它們的訪問次序對各個節點的左、右孩子順序訪問。這樣一層一層進行,先訪問的節點其左、右孩子也要先訪問,與隊列的操做原則比較吻合,且符合廣度優先搜索的特色。
算法過程:先將根節點進隊,在隊不空時循環;從隊列中出列一個節點,訪問它;若它有左孩子節點,將左孩子節點進隊;若它有右孩子節點,將右孩子節點進隊。如此重複操做直至隊空爲止。
void LevelOrder(BTNode *root) { if (NULL == root) return; queue<BTNode*> queue; queue.push(root); BTNode* p; while (!queue.empty()) { p = queue.front(); queue.pop(); visit(p); if (p->left) queue.push(p->left); if (p->right) queue.push(p->right); } }