二叉樹前序、中序、後序(遞迴 / 非遞迴)遍歷
前語
二叉樹的遍歷是指按一定次序訪問二叉樹中的每一個結點,且每個節點僅被訪問一次。
前序遍歷
若二叉樹非空,則進行以下次序的遍歷:
根節點—>根節點的左子樹—>根節點的右子樹
若要遍歷左子樹和右子樹,仍然需要按照以上次序進行,所以前序遍歷也是一個遞迴定義。
(1) 遞迴的前序遍歷
//前序遞迴遍歷
void PreOrder(BinTreeNode* pRoot)
{
//根節點為空,直接返回
if (pRoot == NULL)
return;
printf("%c ", pRoot->_data);//訪問根節點
PreOrder(pRoot->_left);//訪問左子樹
PreOrder(pRoot->_right);//訪問右子樹
}
(2) 非遞迴前序遍歷
按照前序遍歷的規則:訪問根節點後,應根據根節點的left指標進入左子樹進行遍歷,遍歷結束後在進入右子樹進行遍歷。那如果不使用遞迴演算法時,訪問根節點後,進入左子樹遍歷結束後,現在需要進入右子樹,所以需要將右子樹的結點資訊保留下來,以便我後面遍歷右子樹使用。再有觀察可以知道,根節點的右子樹是遍歷完整個左子樹後,才去訪問的,就發現了“ 先儲存,後使用 ”,與我們所熟悉棧的特性“ 後進先出 ”一致,所以我們使用棧來儲存右子樹的資訊。
分析步驟
- 將根節點入棧,重複以下步驟
- 訪問棧頂元素(根節點),然後元素出棧
- 將根節點的右子樹入棧儲存
- 將根節點的左子樹入棧儲存(這樣就再以左子樹節點為根節點往下遍歷重複)
- 直至節點為NULL,棧為空,遍歷結束
//非遞迴前序遍歷
void PreOrderNor(BinTreeNode* pRoot)
{
Stack s;
BinTreeNode* cur = NULL;
if (pRoot == NULL)
return;
InitStack(&s);
//根節點入棧
PushStack(& s, pRoot);
//開始迴圈操作
while (!EmptyStack(&s))
{
//訪問棧頂元素(根節點)
cur = TopStack(&s);
printf("%c ", cur->_data);
//根節點出棧
PopStack(&s);
//將根節點的右子樹入棧(因為最後訪問的右子樹)
if (cur->_right != NULL)
PushStack(&s, cur->_right);
//將根節點的左子樹入棧
if (cur->_left != NULL)
PushStack(&s, cur->_left);
}
}
中序遍歷
若二叉樹非空,則進行以下次序的遍歷:
根節點的左子樹—>根節點—>根節點的右子樹
(1) 遞迴的中序遍歷
void InOrder(BinTreeNode* pRoot)
{
//根節點為空,直接返回
if (pRoot == NULL)
return;
InOrder(pRoot->_left);//訪問左子樹
printf("%c ", pRoot->_data);//訪問根節點
InOrder(pRoot->_right);//訪問右子樹
}
(2) 非遞迴的中序遍歷
中序遍歷的第一個節點是最左側的節點,所以需要遍歷查詢以cur為根的最左側的結點(最左側的節點無左孩子)。找到後迴圈跳出,取棧頂元素,因為沒有左孩子,所以訪問cur指向的根節點,然後遍歷以cur為根的右子樹。重複上述迴圈,直至根節點的左子樹遍歷完成後,棧為空,要進入根節點的右子樹遍歷,此時就斷定大的迴圈條件不應該和前序遍歷相同。
因此應該再加一個條件和它是或的關係(二者至少一個成立就好),所以我們便發現比那裡完左子樹後和根節點後,雖然棧為空,但cur的指向不為空,且當二叉樹都遍歷結束後,cur為空、棧也為空,迴圈跳出,遍歷結束。
//非遞迴的中序遍歷:左子樹-->根-->右子樹
//--->中序遍歷的便利的第一個節點是最左下的結點
//因此需要找最坐下的節點,並將查詢最坐下根節點路徑上的結點壓入棧中儲存起來
void InOrderNor(BinTreeNode* pRoot)
{
Stack s;
BinTreeNode* cur = NULL;
if (pRoot == NULL)
return;
InitStack(&s);
cur = pRoot;
while (!EmptyStack(&s) || cur)
{
//1.找最左下的結點-->需要將所經過路徑的結點壓入棧中儲存-->最左下的根節點沒有左孩子
while (cur)
{
PushStack(&s, cur);
cur = cur->_left;
}
//2.取棧頂元素,遍歷以cur為根的節點
cur = TopStack(&s);
PopStack(&s);
//遍歷以cur為根的二叉樹的根節點
printf("%c ", cur->_data);
//遍歷以cur為根的二叉樹的左節點
cur = cur->_right;
}
}
後序遍歷
若二叉樹非空,則進行以下次序的遍歷:
根節點的左子樹—>根節點的右子樹—>根節點
(1) 遞迴的後序遍歷
//遞迴的後序遍歷
void PostOrder(BinTreeNode* pRoot)
{
if (pRoot == NULL)
return;
PostOrder(pRoot->_left);
PostOrder(pRoot->_right);
printf("%c ", pRoot->_data);
}
(2) 非遞迴的後序遍歷
後序遍歷的第一個節點也是最左側的節點(左孩子為空)。要先找到最左側的元素並儲存其所經路徑的所有節點,以便於後面的遍歷。
只有在遍歷完左子樹和右子樹後,才能訪問根節點。所以如何判斷是夠遍歷完左右子樹成為關鍵。因此我們會想到設定一個標誌pFlag指向最近的被遍歷的節點。當棧頂指標指向的元素的右子樹(因為相對於左右子樹來說,只要訪問過右子樹,左子樹一定已經訪問過了)與標記指標相同時,該節點已經被遍歷。
//非遞迴的後序遍歷:左子樹-->右子樹-->根節點
void PostOrderNor(BinTreeNode* pRoot)
{
BinTreeNode* cur = pRoot;
BinTreeNode* pFlag = NULL;
BinTreeNode* pTop = NULL;
Stack s;
if (pRoot == NULL)
return;
InitStack(&s);
while (cur || !EmptyStack(&s))
{
//仍然是找最左下的元素(無左孩子),並儲存其所經路徑中最左側的結點(入棧)
while (cur)
{
PushStack(&s, cur);
cur = cur->_left;
}
//pTop為根的二叉樹的根節點,根節點不能直接遍歷,除非pTop的右子樹為空
pTop = TopStack(&s);
//如果它沒有右孩子或者節點已遍歷過了-->元素出棧,直接訪問其根節點-->返回上一層
//因為返回上一層後,會繼續遍歷其左右子樹,會發生死迴圈,所以需要標記其最近訪問的一個元素
if (pTop->_right == NULL || pTop->_right == pFlag)
{
PopStack(&s);
printf("%c ", pTop->_data);
pFlag = pTop;
}
else //如果它有右孩子-->以其右孩子為根節點重複以上步驟
cur = pTop->_right;
}
}
標頭檔案、測試函式、程式碼執行結果
//BinTree.h
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
typedef char DataType;
typedef struct BinTreeNode
{
struct BinTreeNode* _left; //當前節點的左子樹
struct BinTreeNode* _right; //當前節點的右子樹
DataType _data; //當前節點的資料域
}BinTreeNode;
//test.c
void test()
{
BinTreeNode* pRoot = NULL;
char* str = "ABD###CE##F";
CreateBinTree(&pRoot, str, strlen(str), '#');
printf("遞迴前序遍歷為:");
PreOrderBinTree(pRoot);
printf("\n非遞迴前序遍歷為:");
PreOrderNor(pRoot);
printf("\n遞迴中序遍歷為:");
InOrderBinTree(pRoot);
printf("\n非遞迴中序遍歷為:");
InOrderNor(pRoot);
printf("\n遞迴後序遍歷為:");
PostOrderBinTree(pRoot);
printf("\n非遞迴後序遍歷為:");
PostOrderNor(pRoot);
printf("\n\n");
}