二叉樹——遍歷篇(c++)
二叉樹——遍歷篇
二叉樹很多算法題都與其遍歷相關,筆者經過大量學習並進行了思考和總結,寫下這篇二叉樹的遍歷篇。
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++)