【前言】樹的遍歷,根據訪問自身和其子節點之間的順序關係,分爲前序,後序遍歷。對於二叉樹,每一個節點至多有兩個子節點(特別的稱爲左,右子節點),又有中序遍歷。因爲樹自身具備的遞歸性,這些遍歷函數使用遞歸函數很容易實現,代碼也很是簡潔。藉助於數據結構中的棧,能夠把樹遍歷的遞歸函數改寫爲非遞歸函數。html
在這裏我思考的問題是,很顯然,循環能夠改寫爲遞歸函數。遞歸函數是否藉助棧這種數據結構改寫爲循環呢。由於函數調用中,call procedure stack 中存儲了流程的 context,調用和返回至關於根據調用棧中的 context 進行跳轉。而採用 stack 數據結構時,主要仍是一個順序循環結構,主要經過 continue 實現流程控制。node
首先,給出遍歷二叉樹的序的定義:數組
(1)前序遍歷:當前節點,左子節點,右子節點;數據結構
(2)中序遍歷:左子節點,當前節點,右子節點;ide
(3)後序遍歷:左子節點,右子節點,當前節點。函數
對二叉查找樹 BST 來講,中序遍歷的輸出,是排序結果。因此這裏我以一個 BST 的中序遍歷爲主要例子說明問題。一個簡單的 BST 以下圖所示(爲了保證美觀精確,下圖由我臨時編寫的一個 VC 窗口程序繪製爲樣本進行加工獲得的):post
其中序遍歷的輸出爲:1,2,3,4,5,6,7,8,9;url
首先給出中序遍歷的遞歸函數,代碼以下:spa
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 的順序,就能夠獲得相應的遍歷序。3d
如今,引入棧數據結構,它是一個元素爲節點指針的數組,將上面的遞歸函數改寫爲非遞歸函數。中序遍歷的基本方法是:
(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; }
能夠看到,釋放樹(FreeTree)這個函數,就是按照後序遍歷的順序進行釋放的。
【補充】和本文相關的我寫的其餘博客文章:
(1)採用路徑模型實現遍歷二叉樹的方法。2013-5-18;
(2)[非原創]樹和圖的遍歷。2008-8-10;
【後記】
獻給曾經向我請教「採用非遞歸方法遍歷樹」的 小玉(littlehead)學妹。