標籤:span 遍曆二叉樹 順序 方便 基本 廣度 邏輯 優先 設定
二叉樹——遍曆篇
二叉樹很多演算法題都與其遍曆相關,筆者經過大量學習並進行了思考和總結,寫下這篇二叉樹的遍曆篇。
1、二叉樹資料結構及訪問函數
#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 << " ";}
2、二叉樹的遍曆——深度優先遍曆(DFS)(先序、中序、後序)2.1、具體遍曆順序
- 先序遍曆:訪問根節點、先序遍曆左孩子、先序遍曆右孩子 (根、左、右)
- 中序遍曆:中序遍曆左孩子、訪問根節點、中序遍曆右孩子 (左、根、右)
- 後序遍曆:後序遍曆左孩子、後序遍曆右孩子、訪問根節點 (左、右、根)
2.2、遞迴遍曆
/** * 先序遍曆二叉樹*/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); }}
2.3、非遞迴遍曆——藉助棧
- 藉助棧,可以實現非遞迴遍曆。
- 在這裡三種非遞迴遍曆都總結和介紹一種演算法思路,其棧中儲存的節點可以用於路徑搜尋類的題目,即儲存著從根節點到當前訪問節點最短路徑的所有節點資訊,以*標記。
- 介紹僅用於遍曆訪問的簡單思路,棧中資訊難以應用於搜尋路徑類的題目。
2.31 先序非遞迴遍曆* PreOrder_1a
演算法過程:
由先序遍曆過程可知,先序遍曆的開始節點是根節點,然後用指標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_1b
演算法過程:此演算法與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 和 PreOrder_2b 演算法思路大體相同,PreOrder_2a 實現比較簡潔
演算法過程:
用指標p指向當前要處理的節點,沿著左下方向逐一訪問並壓棧,直至指標P為空白,棧頂節點為最左下節點。然後p指向棧頂節點的右節點(不管是否空);若右節點為空白,則繼續彈棧。若右節點非空,則按上述同樣處理右節點。如此重複操作直至棧空為止。
缺陷:PreOrder_2 當訪問棧頂節點的右節點時,會丟失當前棧頂節點資訊,導致從根節點到當前棧頂節點的右節點路徑不完整。
優點:演算法思路清晰易懂,邏輯簡單。
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(); } } }
PreOrder_3
演算法過程:
用指標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); }}
2.32 中序非遞迴遍曆
中序遍曆非遞迴演算法要把握訪問棧頂節點的時機。
* InOrder_1
演算法過程:
用指標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());}
InOrder_2a && InOrder_2b
演算法過程:
用指標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(); } }}
2.33 後序非遞迴遍曆*PostOrder_1
演算法過程:
用指標cur指向當前要處理的節點。先掃描(並非訪問)根節點的所有左節點並將它們一一進棧,直至棧頂節點為最左下節點,指標cur為空白。此時有兩種情況。(1)棧頂節點的右節點為空白或已訪問,訪問當前棧頂節點(訪問時機很重要!),用指標p儲存剛剛訪問過的節點(初值為NULL),彈棧;(2)棧頂節點有未被訪問的右節點,設定flag,退出當前訪問搜尋迴圈。如此重複處理,直至棧空為止。
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());}
PostOrder_2
演算法過程:
先把根節點壓棧,用指標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); } }}
3、二叉樹的遍曆——廣度優先遍曆(BFS)
在進行層次遍曆時,對某一層的節點訪問完後,再按照對它們的訪問次序對各個節點的左、右孩子順序訪問。這樣一層一層進行,先訪問的節點其左、右孩子也要先訪問,與隊列的操作原則比較吻合,且符合廣度優先搜尋的特點。
演算法過程:先將根節點進隊,在隊不空時迴圈;從隊列中出列一個節點,訪問它;若它有左孩子節點,將左孩子節點進隊;若它有右孩子節點,將右孩子節點進隊。如此重複操作直至隊空為止。
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); } }
原創所有,轉載務必註明出處。
?
二叉樹——遍曆篇(c++)