1. 程式人生 > >二叉樹——遍歷篇(c++)

二叉樹——遍歷篇(c++)

比較 方便 || 遍歷二叉樹 找到 保存 們的 order out

二叉樹——遍歷篇

二叉樹很多算法題都與其遍歷相關,筆者經過大量學習並進行了思考和總結,寫下這篇二叉樹的遍歷篇。

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++)