標籤:des style blog http color 使用 os 資料
【前言】樹的遍曆,根據訪問自身和其子節點之間的循序關聯性,分為前序,後序遍曆。對於二叉樹,每個節點至多有兩個子節點(特別的稱為左,右子節點),又有中序遍曆。由於樹自身具有的遞迴性,這些遍曆函數使用遞迴函式很容易實現,代碼也非常簡潔。藉助於資料結構中的棧,可以把樹遍曆的遞迴函式改寫為非遞迴函式。
在這裡我思考的問題是,很顯然,迴圈可以改寫為遞迴函式。遞迴函式是否藉助棧這種資料結構改寫為迴圈呢。因為函數調用中,call procedure stack 中儲存了流程的 context,調用和返回相當於根據調用棧中的 context 進行跳轉。而採用 stack 資料結構時,主要還是一個順序迴圈結構,主要通過 continue 實現流程式控制制。
首先,給出遍曆二叉樹的序的定義:
(1)前序走訪:當前節點,左子節點,右子節點;
(2)中序遍曆:左子節點,當前節點,右子節點;
(3)後序遍曆:左子節點,右子節點,當前節點。
對二叉尋找樹 BST 來說,中序遍曆的輸出,是排序結果。所以這裡我以一個 BST 的中序遍曆為主要例子說明問題。一個簡單的 BST 如所示:
其中序遍曆的輸出為: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 empty17 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 }
在上面的代碼的 while 迴圈體內,可以分為三個小的代碼塊:
(1)pop 棧頂的節點,並訪問;
(2)push 左子節點;
(3)push 右子節點;
只要調整 while 迴圈體中的這三個代碼塊的順序,就可以分別實現三種遍曆序。從上面的代碼中,有兩點需要說明:
(1)最後一個代碼塊中的 continue 可以不需要寫,但為了可以調整代碼塊的順序,兩個 continue 都是需要的。
(2)因為前序走訪的邏輯的簡潔性,不藉助於 bVisited 標記,也可以完成遍曆,但為了通用,還是需要這個節點標記。
最後,補充上其他並不重要的方法,建立樹,釋放樹,main 函數的代碼如下:
//左右 Child 定義#define LCHILD 0#define RCHILD 1typedef 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)學妹。儘管,限於個人天賦和能力的有限,我給出的解答遲了一些。