對於一種數據結構而言,遍歷是常見操做。二叉樹是一種基本的數據結構,是一種每一個節點的兒子數目都很少於2的樹。二叉樹的節點聲明以下:html
1 typedef struct TreeNode *PtrToNode; 2 typedef struct TreeNode *BinTree; 3
4 struct TreeNode 5 { 6 int Data; //爲簡單起見,不妨假設樹節點的元素爲int型
7 BinTree Left; 8 BinTree Right; 9 };
二叉樹的遍歷主要有先序遍歷,中序遍歷,後序遍歷,層序遍歷四種方式,下面一一介紹。算法
1. 先序遍歷數據結構
在先序遍歷中,對節點的訪問工做是在它的左右兒子被訪問以前進行的。換言之,先序遍歷訪問節點的順序是根節點-左兒子-右兒子。因爲樹能夠經過遞歸來定義,因此樹的常見操做用遞歸實現經常是方便清晰的。遞歸實現的代碼以下:ide
1 void PreOrderTraversal(BinTree BT) 2 { 3 if( BT ) 4 { 5 printf(「%d\n」, BT->Data); //對節點作些訪問好比打印
6 PreOrderTraversal(BT->Left); //訪問左兒子
7 PreOrderTraversal(BT->Right); //訪問右兒子
8 } 9 }
由遞歸代碼能夠看出,該遞歸爲尾遞歸(尾遞歸即遞歸形式在函數末尾或者說在函數即將返回前)。尾遞歸的遞歸調用須要用棧存儲調用的信息,當數據規模較大時容易越出棧空間。雖然如今大部分的編譯器可以自動去除尾遞歸,可是即便如此,咱們不妨本身去除。非遞歸先序遍歷算法基本思路:使用堆棧函數
a. 遇到一個節點,訪問它,而後把它壓棧,並去遍歷它的左子樹;url
b. 當左子樹遍歷結束後,從棧頂彈出該節點並將其指向右兒子,繼續a步驟;spa
c. 當全部節點訪問完即最後訪問的樹節點爲空且棧空時,中止。.net
實現代碼以下:設計
void PreOrderTraversal(BinTree BT) { BinTree T = BT; Stack S = CreatStack(MAX_SIZE); //建立並初始化堆棧S
while(T || !IsEmpty(S)) { while(T) //一直向左並將沿途節點訪問(打印)後壓入堆棧
{ printf("%d\n", T->Data); Push(S, T); T = T->Left; } if (!IsEmpty(S)) { T = Pop(S); //節點彈出堆棧
T = T->Right; //轉向右子樹
} }
}
中序遍歷的遍歷路徑與先序遍歷徹底同樣。其實現的思路也與先序遍歷很是類似。其主要的不一樣點是訪問節點順序不一樣:中序遍歷是訪問完全部左兒子後再訪問根節點,最後訪問右兒子,即爲左兒子-根節點-右兒子。指針
遞歸實現的代碼以下:
void InOrderTraversal(BinTree BT) { if(BT) { InOrderTraversal(BT->Left); printf("%d\n", BT->Data); InOrderTraversal(BT->Right); } }
非遞歸輔助棧實現代碼以下:
void InOrderTraversal(BinTree BT) { BinTree T = BT; Stack S = CreatStack(MaxSize); //建立並初始化堆棧S
while(T || !IsEmpty(S))
{ while(T) //一直向左並將沿途節點壓入堆棧
{ Push(S,T); T = T->Left; } if(!IsEmpty(S)) { T = Pop(S); //節點彈出堆棧
printf("%d\n", T->Data); //(訪問) 打印結點
T = T->Right; //轉向右子樹
}
} }
非遞歸不用輔助棧實現中序遍歷:
試設計一個非遞歸算法,按中根順序遍歷非線索二叉樹,但不得用任何輔助棧。在執行算法期間,容許改變左孩子指針和右孩子指針的值。
算法:右線索化+回溯
代碼以下(關於二叉樹的創建請戳我):
/* 輸入:ABDH##I##E##CF#J##G## */ #include <cstdio> typedef struct BTNode* Position; typedef Position BTree; typedef char ElementType; struct BTNode { ElementType data; Position lChild, rChild; }; BTree CreateBTree(void); void Inorder(BTree bt); int main() { BTree bt = CreateBTree(); Inorder(bt); return 0; } void Inorder(BTree bt) { Position p = bt; while (p) { Position pLeft = p->lChild; if (pLeft) { while (pLeft->rChild && pLeft->rChild != p) //找到以p爲根結點的樹的最右孩子 pLeft = pLeft->rChild; if (pLeft->rChild == NULL) //線索化 { pLeft->rChild = p; p = p->lChild; continue; } else //線索化後已被訪問 { pLeft->rChild = NULL; //釋放指向根節點(祖先)的指針 } } printf("%c ", p->data); //打印 p = p->rChild; //向上回溯或者轉向右子樹 } printf("\n"); } BTree CreateBTree() //按照先序序列創建二叉樹 { BTree bt = NULL; char ch; scanf("%c", &ch); if (ch != '#') //'#'表明空節點 { bt = new BTNode; bt->data = ch; bt->lChild = CreateBTree(); bt->rChild = CreateBTree(); } return bt; }
運行結果:
參考博客:http://m.blog.csdn.net/blog/Raito__/40618257
後序遍歷與中序遍歷,先序遍歷的路徑也徹底同樣。主要的不一樣點是後序遍歷訪問節點的順序是先訪問左兒子和右兒子,最後訪問節點,即左兒子-右兒子-根節點。
遞歸實現思路與中序遍歷和先序遍歷類似,代碼以下:
void PostOrderTraversal(BinTree BT) { if (BT) { PostOrderTraversal(BT->Left); PostOrderTraversal(BT->Right); printf("%d\n", BT->Data); } }
後序遍歷的非遞歸實現
思路一:
對於一個節點而言,要實現訪問順序爲左兒子-右兒子-根節點,能夠利用後進先出的棧,在節點不爲空的前提下,依次將根節點,右兒子,左兒子壓棧。故咱們須要按照根節點-右兒子-左兒子的順序遍歷樹,而咱們已經知道先序遍歷的順序是根節點-左兒子-右兒子,故只需將先序遍歷的左右調換並把訪問方式打印改成壓入另外一個棧便可。最後一塊兒打印棧中的元素。代碼以下:
void PostOrderTraversal(BinTree BT) { BinTree T = BT; Stack S1 = CreatStack(MAX_SIZE); //建立並初始化堆棧S1
Stack S2 = CreatStack(MAX_SIZE); //建立並初始化堆棧S2
while(T || !IsEmpty(S1)) { while(T) //一直向右並將沿途節點訪問(壓入S2)後壓入堆棧S1
{ Push(S2, T); Push(S1, T); T = T->Right; } if (!IsEmpty(S1)) { T = Pop(S1); //節點彈出堆棧
T = T->Left; //轉向左子樹
} } while(!IsEmpty(S2)) //訪問(打印)S2中元素
{ T = Pop(S2); printf("%d\n", T->Data); } }
思路一的優勢是因爲利用了先序遍歷的思想,代碼較簡潔,思路較清晰。缺點是須要用一個棧來存儲樹的全部節點,空間佔用較大。
思路二:
要訪問一個節點的條件上一個訪問的節點是右兒子。咱們能夠增長一個變量Prev來判斷當前節點Curr的上一個節點與它的關係來執行相應的操做。
代碼以下:
void PostOrderTraversal(BinTree BT) { if(BT == NULL) return ; Stack S = CreatStack(MAX_SIZE); BinTree Prev = NULL , Curr = NULL; //初始化
s.push(BT); while(!IsEmpty(S)) { Curr = Top(S); //將棧頂元素賦給Curr
if(Prev == NULL || Prev->Left == Curr || Prev->Right == Curr) //若Prev爲NULL或是Curr的父節點
{ if(Curr->Left != NULL) Push(S, Curr->Left); else if(Curr->Right != NULL) Push(S, Curr->Right); } else if(Curr->Left == Prev) //若Prev是Curr的左兒子
{ if(Curr->Right != NULL) Push(S, Curr->Right); } else { printf("%d\n", Curr->Data); //訪問當前節點
Pop(S); //訪問後彈出
} Prev = Curr; //處理完當前節點後將Curr節點變爲Prev節點
} }
二叉樹遍歷的核心問題是二維結構的線性化。咱們經過節點訪問其左右兒子時,存在的問題是訪問左兒子後,右兒子怎麼訪問。所以咱們須要一個存儲結構保存暫時不訪問的節點。前面三種遍歷方式的非遞歸實現,咱們是經過堆棧來保存。事實上也能夠經過隊列來保存。
隊列實現的基本思路:遍歷從根節點開始,首先將根節點入隊,而後執行循環:節點出隊,訪問(訪問)根節點,將左兒子入隊,將右兒子入隊,直到隊列爲空中止。
這種遍歷方式的結果是將二叉樹從上到下,從左至右一層一層的遍歷,即層序遍歷,代碼實現以下:
void LevelOrderTraversal(BinTree BT) { BinTree T; Queue Q; //聲明一個隊列
if (BT == NULL) return; //若是樹爲空,直接返回
Q = CreatQueue(MAX_SIZE); //建立並初始化隊列
AddQ(Q, BT); //將根節點入隊
while (!IsEmpty(Q)) { T = DeleteQ(Q); //節點出隊
printf("%d\n", T->Data); //訪問出隊的節點
if (T->Left) AddQ(Q, T->Left); //若左兒子不爲空,將其入隊
if (T->Right) AddQ(Q, T->Right) //若右兒子不爲空,將其入隊
} }
理解上述四種二叉樹遍歷方式後,不妨來小試牛刀:List Leaves, Tree Traversals Again.