采用棧數據結構的二叉樹非遞歸遍歷


  【前言】樹的遍歷,根據訪問自身和其子節點之間的順序關系,分為前序,后序遍歷。對於二叉樹,每個節點至多有兩個子節點(特別的稱為左,右子節點),又有中序遍歷。由於樹自身具有的遞歸性,這些遍歷函數使用遞歸函數很容易實現,代碼也非常簡潔。借助於數據結構中的棧,可以把樹遍歷的遞歸函數改寫為非遞歸函數。

 

  在這里我思考的問題是,很顯然,循環可以改寫為遞歸函數。遞歸函數是否借助棧這種數據結構改寫為循環呢。因為函數調用中,call procedure stack 中存儲了流程的 context,調用和返回相當於根據調用棧中的 context 進行跳轉。而采用 stack 數據結構時,主要還是一個順序循環結構,主要通過 continue 實現流程控制。

 

  首先,給出遍歷二叉樹的序的定義:

 

  (1)前序遍歷:當前節點,左子節點,右子節點;

  (2)中序遍歷:左子節點,當前節點,右子節點;

  (3)后序遍歷:左子節點,右子節點,當前節點。

 

  對二叉查找樹 BST 來說,中序遍歷的輸出,是排序結果。所以這里我以一個 BST 的中序遍歷為主要例子說明問題。一個簡單的 BST 如下圖所示(為了保證美觀精確,下圖由我臨時編寫的一個 VC 窗口程序繪制為樣本進行加工得到的):

 

  

 

  其中序遍歷的輸出為:1,2,3,4,5,6,7,8,9;

 

  首先給出中序遍歷的遞歸函數,代碼如下:

 

 1 typedef struct tagNODE
 2 {
 3     int nVal;
 4     int bVisited; //是否被訪問過
 5     struct tagNODE *pLeft;
 6     struct tagNODE *pRight;
 7 } NODE, *LPNODE;
 8 
 9 //中序遍歷二叉樹(遞歸版本)
10 void Travel_Recursive(LPNODE pNode)
11 {
12     if(pNode != NULL)
13     {
14         Travel_Recursive(pNode->pLeft);
15         _tprintf(_T("%ld, "), pNode->nVal);
16         Travel_Recursive(pNode->pRight);
17     }
18 }

 

  很明顯,對應於前面給出的定義,只需要調整上述代碼中行號為 14,15,16 的順序,就可以得到相應的遍歷序。

 

  現在,引入棧數據結構,它是一個元素為節點指針的數組,將上面的遞歸函數改寫為非遞歸函數。中序遍歷的基本方法是:

 

  (1)將根節點 push 入棧;

  (2)當棧不為空時,重復(3)到(5)的操作:

  (3)偷窺棧頂部節點,如果節點的左子節點不為 NULL,且沒有被訪問,則將其左子節點 push 入棧,並跳到(3)。

  (4)當被偷窺的節點沒有左子樹,pop 該節點出棧,並訪問它(同時標記該節點為已訪問狀態)。

  (5)當該節點的右子節點不為空,將其右子節點 push 入棧,並跳到(3)。

 

  根據以上方法,給出非遞歸函數的中序遍歷版本代碼如下:

 

 1 typedef struct tagNODE
 2 {
 3     int nVal;
 4     int bVisited; //是否被訪問過
 5     struct tagNODE *pLeft;
 6     struct tagNODE *pRight;
 7 } NODE, *LPNODE;
 8 
 9 //輔助數據結構
10 LPNODE g_Stack[256];
11 int g_nTop;
12 
13 //遍歷二叉樹,借助於stack數據結構的非遞歸版本
14 void TravelTree()
15 {
16     //while the stack is not empty
17     while(g_nTop >= 0)
18     {
19         //peek the top node in stack;
20         LPNODE pNode = g_Stack[g_nTop];
21 
22         //push left child;
23         if(pNode->pLeft != NULL && !pNode->pLeft->bVisited)
24         {
25             ++g_nTop;
26             g_Stack[g_nTop] = pNode->pLeft;
27             continue;
28         }
29 
30         //pop and visit it;
31         _tprintf(_T("%ld, "), pNode->nVal);
32         pNode->bVisited = 1;
33         --g_nTop; 
34 
35         //push right child;
36         if(pNode->pRight != NULL && !pNode->pRight->bVisited)
37         {
38             ++g_nTop;
39             g_Stack[g_nTop] = pNode->pRight;
40             continue;
41         }       
42     }
43 }

 

  以前面的 BST 為例,在非遞歸函數中,棧狀態的動態變化如下圖所示(下圖主要由 Excel 和 Photoshop 制作):

  

  在上面的代碼的 while 循環體內,可以分為三個小的代碼塊:

 

  (1)pop 棧頂的節點,並訪問此節點 (line 30 ~ 33);

  (2)push 左子節點 (line 22 ~ 28);

  (3)push 右子節點 (line 35 ~ 41);

 

  只要調整 while 循環體中的這三個代碼塊的順序,就可以分別實現三種遍歷序。例如,前序:(1)(2)(3);后序:(2)(3)(1)。

  從上面的代碼中,有兩點需要說明:

 

  (1)最后一個代碼塊中的 continue 可以不需要寫,但為了可以調整代碼塊的順序,兩個 continue 都是需要的。

  (2)因為前序遍歷的邏輯的簡潔性,不借助於 bVisited 標記,也可以完成遍歷,但為了通用,還是需要這個節點標記。

 

  最后,補充上其他並不重要的方法,創建樹,釋放樹,main 函數的代碼如下(把已有所有代碼拼在一起即構成完整的 Demo 程序):

 

//左右 Child 定義
#define LCHILD        0
#define RCHILD        1

typedef struct tagNODE
{
    int nVal;
    int bVisited; //是否被訪問過
    struct tagNODE *pLeft;
    struct tagNODE *pRight;
} NODE, *LPNODE;

LPNODE g_Stack[256];
int g_nTop;

LPNODE InsertNode(LPNODE pParent, int nWhichChild, int val)
{
    LPNODE pNode = (LPNODE)malloc(sizeof(NODE));
    memset(pNode, 0, sizeof(NODE));
    pNode->nVal = val;

    if(pParent != NULL)
    {
        if(nWhichChild == LCHILD)
            pParent->pLeft = pNode;
        else
            pParent->pRight = pNode;
    }
    return pNode;
}

//遞歸釋放二叉樹的內存
void FreeTree(LPNODE pRoot)
{
    if(pRoot != NULL)
    {
        FreeTree(pRoot->pLeft);
        FreeTree(pRoot->pRight);
        //_tprintf(_T("freeing Node (%ld) ...\n"), pRoot->nVal);
        free(pRoot);
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    //索引為 0 的元素不使用。
    LPNODE pNodes[10] = { 0 };

    pNodes[1] = InsertNode(pNodes[0], LCHILD, 7);
    pNodes[2] = InsertNode(pNodes[1], LCHILD, 4);
    pNodes[3] = InsertNode(pNodes[1], RCHILD, 9);
    pNodes[4] = InsertNode(pNodes[2], LCHILD, 2);
    pNodes[5] = InsertNode(pNodes[2], RCHILD, 6);
    pNodes[6] = InsertNode(pNodes[3], LCHILD, 8);
    pNodes[7] = InsertNode(pNodes[4], LCHILD, 1);
    pNodes[8] = InsertNode(pNodes[4], RCHILD, 3);
    pNodes[9] = InsertNode(pNodes[5], LCHILD, 5);

    //push 根節點
    g_nTop = 0;
    g_Stack[g_nTop] = pNodes[1];

    TravelTree();
    _tprintf(_T("\n"));

    Travel_Recursive(pNodes[1]);
    _tprintf(_T("\n"));

    FreeTree(pNodes[1]);
    return 0;
}
View Code

 

  可以看到,釋放樹(FreeTree)這個函數,就是按照后序遍歷的順序進行釋放的。

 

  【補充】和本文相關的我寫的其他博客文章:

 

  (1)采用路徑模型實現遍歷二叉樹的方法。2013-5-18;

  (2)[非原創]樹和圖的遍歷。2008-8-10;

 

  【后記】

  獻給曾經向我請教“采用非遞歸方法遍歷樹”的 小玉(littlehead)學妹。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM