[資料結構--樹] 樹的四種遍歷方式
一、引言
前面介紹了樹的一些基本概念,接下來將實現樹的各種操作。樹主要應用廣的還是二叉樹,因此主要實現二叉樹的各類操作,包括建立、刪除、查詢、遍歷等等。
其中樹的基本操作最重要的是遍歷,因此首先專門講解並實現樹的遍歷演算法。
二、遍歷方式
樹共有4種遍歷方式,分別為:先序遍歷、中序遍歷、後序遍歷、層序遍歷。每種遍歷演算法都有遞迴和非遞迴兩種實現方式,下面分別介紹。
1、先序遍歷
先序遍歷的遍歷順序為根->左->右,如上圖先序遍歷為3 5 1 4 7 2 6。
(1) 遞迴實現
先序遍歷遞迴實現是先遍歷根結點,其次遍歷左子樹、最後遍歷右子樹。並且在遍歷左右子樹的時候,依舊遵循根->左->右的順序。
/* 先序遍歷--遞迴實現。 */
void PreOrderTraverse_Recursive(BiTree *T)
{
if (T != NULL) {
display(T); // 先訪問根結點
PreOrderTraverse_Recursive(T->lchild); // 再訪問左孩子
PreOrderTraverse_Recursive(T->rchild); // 再訪問右孩子
}
return;
}
(2) 非遞迴
遞迴演算法,其實本質底層都是利用“棧”這個資料結構實現的非遞迴邏輯。因此現在要實現非遞迴演算法,無外乎是利用“棧”實現遞迴的底層邏輯。
A、申請一個棧。
B、當樹非空時,根結點壓棧。
C、儲存並列印棧頂,同時棧頂彈棧(彈棧前,先要進行D、E步驟)。
D、判斷當前棧頂的左孩子,若非空,則將其壓入棧中。
E、判斷當前棧頂的右孩子,若非空,則將其壓入棧中。
F、判斷棧是否為空,若非空,重複C-E步驟,直到棧空結束遞迴。
/* 先序遍歷--非遞迴實現。 */
void PreOrderTraverse_NoRecursive(BiTree *T)
{
BiTree *stack[N]; // 順序棧
int top = -1; // 棧頂指標
BiTree *p;
stack[++top] = T; // 首先,根入棧
while (top != -1) { // 當棧非空
p = stack[top]; // 獲取棧頂
top--; // 棧頂出棧
while (p != NULL) {
display(p); // 列印棧頂
if (p->rchild != NULL) { // 如果該結點有右孩子,則右孩子入棧
stack[++top] = p->rchild;
}
p = p->lchild; // p需要永遠指向當前結點的一個左孩子
}
}
return;
}
2、中序遍歷
中序遍歷的遍歷順序為左->根->右,如上圖中序遍歷為1 5 4 3 2 7 6。
(1) 遞迴實現
中序遍歷遞迴實現是先遍歷左子樹,其次遍歷根結點、最後遍歷右子樹。並且在遍歷左右子樹的時候,依舊遵循左->根->右的順序。
/* 中序遍歷--遞迴實現。 */
void INOrderTraverse_Recursive(BiTree *T)
{
if (T != NULL) {
INOrderTraverse_Recursive(T->lchild);
display(T);
INOrderTraverse_Recursive(T->rchild);
}
return;
}
(2) 非遞迴
A、申請一個棧。
B、當樹非空時,根結點壓棧。
C、當前棧頂的左孩子入棧。
D、重複C步驟,直到當前棧頂的左孩子為空。
E、儲存並列印棧頂,同時棧頂彈棧(彈棧前,先要進行F步驟)。
F、當前棧頂的右孩子入棧。
G、判斷棧是否為空,若非空,重複C-F步驟,直到棧空結束遞迴。
/* 中序遍歷--非遞迴實現。 */
void INOrderTraverse_NoRecursive(BiTree *T)
{
BiTree *stack[N]; // 順序棧
int top = -1; // 棧頂指標
BiTree *p;
stack[++top] = T; // 首先,根入棧
while (top != -1) { // 當棧非空
p = stack[top];
while (p != NULL) {
stack[++top] = p->lchild; // 當前結點的左孩子入棧,沒有則入的是NULL
p = stack[top];
}
top--; // 前一個while迴圈結束時,此時的棧頂一定為NULL,將NULL彈出來
if (top != -1) {
p = stack[top]; // 獲取棧頂
top--;
display(p);
stack[++top] = p->rchild; // 將p的右孩子入棧
}
}
return;
}
3、後序遍歷
後序遍歷的遍歷順序為左->右->根,如上圖後序遍歷為1 4 5 2 6 7 3。
(1) 遞迴實現
後序遍歷遞迴實現是先遍歷左子樹,其次遍歷右子樹、最後遍歷根結點。並且在遍歷左右子樹的時候,依舊遵循左->右->根的順序。
/* 後序遍歷--遞迴實現。 */
void PostOrderTraverse_Recursive(BiTree *T)
{
if (T != NULL) {
PostOrderTraverse_Recursive(T->lchild);
PostOrderTraverse_Recursive(T->rchild);
display(T);
}
return;
}
(2) 非遞迴
後續遍歷的非遞迴實現是最難的一個。因為在後序遍歷中,要保證左孩子和右孩子都已被訪問,並且左孩子在右孩子前訪問,右孩子訪問後才能訪問根結點,這就為流程的控制帶來了困難。
思路:對於任一結點P,將其入棧,然後沿其左子樹一直往下搜尋,直到搜尋到沒有左孩子的結點,此時該結點出現在棧頂,但是此時不能將其出棧並訪問彈出, 因為其右孩子還未被訪問,所以接下來按照相同的規則對其右子樹進行相同的處理。當訪問完其右孩子時,該結點又出現在棧頂,此時可以將其出棧並訪問。這樣就 保證了正確的訪問順序。可以看出,在這個過程中,每個結點都兩次出現在棧頂,只有在第二次出現在棧頂時,才能訪問它。因此需要多設定一個變數標識該結點是否是第一次出現在棧頂。
/* 後序遍歷--非遞迴實現。 */
void PostOrderTraverse_NoRecursive(BiTree *T)
{
Node stack[N]; // 順序棧
int top = -1; // 棧頂指標
int tag; // 標誌位,用來標識某個結點左右孩子情況
BiTree *p;
Node node;
p = T;
while ((p != NULL) || (top != -1)) {
// 為該結點入棧做準備
while (p != NULL) {
node.pt = p;
node.flag = 0;
stack[++top] = node;
p = p->lchild; // 以該結點為根結點,遍歷左孩子
}
node = stack[top]; // 獲取棧頂
top--;
p = node.pt;
tag = node.flag;
// 若為0,則表示該結點尚未遍歷它的右孩子
if (tag == 0) {
node.pt = p;
node.flag = 1;
stack[++top] = node;
p = p->rchild; // 以該結點的右孩子為根結點,重複迴圈
} else { // 若當前棧頂結點的tag=1,說明此結點左右子樹都遍歷完了
display(p);
p = NULL;
}
}
return;
}
4、層序遍歷
層序遍歷就比較簡單了,其遵循先上後下,並且在每一層遵循先左後右的順序,如上圖的層序遍歷為3 5 7 1 4 2 6。
層序遍歷也可以用遞迴演算法實現,但由於該遍歷方式較為簡單,因此只介紹非遞迴實現方式了,遞迴演算法反而顯得有點複雜了。
層序遍歷方式是嚴格按照順序方式遍歷的,因此很顯然需要用到“佇列”這種具有“先進先出”的順序儲存結構。直接從上而下、先左後右的順序入隊出隊即可。
/* 層次遍歷。*/
void OrderTraverse(BiTree *T)
{
BiTree *queue[N]; // 建立一個佇列
BiTree *p;
int front = 0, rear = 0;
queue[rear++] = T; // 根入隊
while (front < rear) { // front<rear時,表示佇列非空
p = queue[front++]; //獲取隊頭,以及隊頭出隊
display(p); // 列印隊頭
// 依次將隊頭的左右孩子入隊。這便是層次遍歷
if (p->lchild != NULL) {
queue[rear++] = p->lchild;
}
if (p->rchild != NULL) {
queue[rear++] = p->rchild;
}
}
return;
}
三、完整程式碼
下面附上完整code
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#define N 20
/* 二叉樹結構定義 */
typedef struct tagBinaryTree {
int data;
struct tagBinaryTree *lchild;
struct tagBinaryTree *rchild;
} BiTree;
void display(BiTree *Tree); /* 列印函式 */
void PreOrderTraverse_Recursive(BiTree *Tree); /* 先序遍歷--遞迴 */
void PreOrderTraverse_NoRecursive(BiTree *Tree); /* 先序遍歷--非遞迴 */
void INOrderTraverse_Recursive(BiTree *Tree); /* 中序遍歷--遞迴 */
void INOrderTraverse_NoRecursive(BiTree *Tree); /* 中序遍歷--非遞迴 */
void PostOrderTraverse_Recursive(BiTree *Tree); /* 後序遍歷--遞迴 */
void PostOrderTraverse_NoRecursive(BiTree *Tree); /* 後序遍歷--非遞迴 */
void OrderTraverse(BiTree *Tree); /* 層次遍歷 */
/* 建立二叉樹 */
/* 這裡,主要是為了講解並實現二叉樹的遍歷演算法,而不是二叉樹的基本操作如:建立、插入、查詢、刪除等等,
因此在這裡手動建立示例中的二叉樹。關於二叉樹更為通用的建立方法後續統一實現。
*/
void CreateBiTree(BiTree **T)
{
/* 思考(重點): 為什麼這裡傳參的時候用的是二級指標**T,底下用(*T)?
為什麼不直接傳個一級指標*T,直接用T?
*/
/* 根結點 */
(*T) = (BiTree *)malloc(sizeof(BiTree));
(*T)->data = 3;
/* 根的左孩子 */
(*T)->lchild = (BiTree *)malloc(sizeof(BiTree));
(*T)->lchild->data = 5;
/* 根的右孩子 */
(*T)->rchild = (BiTree *)malloc(sizeof(BiTree));
(*T)->rchild->data = 7;
/* 根的左孩子的左孩子 */
(*T)->lchild->lchild = (BiTree *)malloc(sizeof(BiTree));
(*T)->lchild->lchild->data = 1;
(*T)->lchild->lchild->lchild = NULL;
(*T)->lchild->lchild->rchild = NULL;
/* 根的左孩子的右孩子 */
(*T)->lchild->rchild = (BiTree *)malloc(sizeof(BiTree));
(*T)->lchild->rchild->data = 4;
(*T)->lchild->rchild->lchild = NULL;
(*T)->lchild->rchild->rchild = NULL;
/* 根的右孩子的左孩子 */
(*T)->rchild->lchild = (BiTree *)malloc(sizeof(BiTree));
(*T)->rchild->lchild->data = 2;
(*T)->rchild->lchild->lchild = NULL;
(*T)->rchild->lchild->rchild = NULL;
/* 跟的右孩子的右孩子 */
(*T)->rchild->rchild = (BiTree *)malloc(sizeof(BiTree));
(*T)->rchild->rchild->data = 6;
(*T)->rchild->rchild->lchild = NULL;
(*T)->rchild->rchild->rchild = NULL;
return;
}
/* 列印結點 */
void display(BiTree *T)
{
printf("%d ", T->data);
}
/********** 先序遍歷: 根->左->右 *********/
/* 遞迴實現。遞迴實現則沒次遍歷時都將左右孩子當做一棵完整的樹來看待。 */
void PreOrderTraverse_Recursive(BiTree *T)
{
if (T != NULL) {
display(T); // 先訪問根結點
PreOrderTraverse_Recursive(T->lchild); // 再訪問左孩子
PreOrderTraverse_Recursive(T->rchild); // 再訪問右孩子
}
return;
}
/* 非遞迴實現。依賴於棧 */
void PreOrderTraverse_NoRecursive(BiTree *T)
{
BiTree *stack[N]; // 順序棧
int top = -1; // 棧頂指標
BiTree *p;
stack[++top] = T; // 首先,根入棧
while (top != -1) { // 當棧非空
p = stack[top]; // 獲取棧頂
top--; // 棧頂出棧
while (p != NULL) {
display(p); // 列印棧頂
if (p->rchild != NULL) { // 如果該結點有右孩子,則右孩子入棧
stack[++top] = p->rchild;
}
p = p->lchild; // p需要永遠指向當前結點的一個左孩子
}
}
return;
}
/********** 中序遍歷: 左->根->右 *********/
/* 遞迴實現。遞迴實現則沒次遍歷時都將左右孩子當做一棵完整的樹來看待。 */
void INOrderTraverse_Recursive(BiTree *T)
{
if (T != NULL) {
INOrderTraverse_Recursive(T->lchild);
display(T);
INOrderTraverse_Recursive(T->rchild);
}
return;
}
/* 非遞迴實現。依賴於棧 */
void INOrderTraverse_NoRecursive(BiTree *T)
{
BiTree *stack[N]; // 順序棧
int top = -1; // 棧頂指標
BiTree *p;
stack[++top] = T; // 首先,根入棧
while (top != -1) { // 當棧非空
p = stack[top];
while (p != NULL) {
stack[++top] = p->lchild; // 當前結點的左孩子入棧,沒有則入的是NULL
p = stack[top];
}
top--; // 前一個while迴圈結束時,此時的棧頂一定為NULL,將NULL彈出來
if (top != -1) {
p = stack[top]; // 獲取棧頂
top--;
display(p);
stack[++top] = p->rchild; // 將p的右孩子入棧
}
}
return;
}
/********** 後序遍歷: 左->右->根 *********/
/* 遞迴實現。遞迴實現則沒次遍歷時都將左右孩子當做一棵完整的樹來看待。 */
void PostOrderTraverse_Recursive(BiTree *T)
{
if (T != NULL) {
PostOrderTraverse_Recursive(T->lchild);
PostOrderTraverse_Recursive(T->rchild);
display(T);
}
return;
}
/* 為每個結點配備相關資訊 */
typedef struct {
BiTree *pt;
int flag;
} Node;
/* 非遞迴實現。依賴於棧 */
void PostOrderTraverse_NoRecursive(BiTree *T)
{
Node stack[N]; // 順序棧
int top = -1; // 棧頂指標
int tag; // 標誌位,用來標識某個結點左右孩子情況
BiTree *p;
Node node;
p = T;
while ((p != NULL) || (top != -1)) {
// 為該結點入棧做準備
while (p != NULL) {
node.pt = p;
node.flag = 0;
stack[++top] = node;
p = p->lchild; // 以該結點為根結點,遍歷左孩子
}
node = stack[top]; // 獲取棧頂
top--;
p = node.pt;
tag = node.flag;
// 若為0,則表示該結點尚未遍歷它的右孩子
if (tag == 0) {
node.pt = p;
node.flag = 1;
stack[++top] = node;
p = p->rchild; // 以該結點的右孩子為根結點,重複迴圈
} else { // 若當前棧頂結點的tag=1,說明此結點左右子樹都遍歷完了
display(p);
p = NULL;
}
}
return;
}
/* 層次遍歷。依賴於佇列 */
void OrderTraverse(BiTree *T)
{
BiTree *queue[N]; // 建立一個佇列
BiTree *p;
int front = 0, rear = 0;
queue[rear++] = T; // 根入隊
while (front < rear) { // front<rear時,表示佇列非空
p = queue[front++]; //獲取隊頭,以及隊頭出隊
display(p); // 列印隊頭
// 依次將隊頭的左右孩子入隊。這便是層次遍歷
if (p->lchild != NULL) {
queue[rear++] = p->lchild;
}
if (p->rchild != NULL) {
queue[rear++] = p->rchild;
}
}
return;
}
int main()
{
BiTree *tree;
/* 建立樹 */
CreateBiTree(&tree);
/***** 先序遍歷 *****/
printf("先序遍歷--遞迴實現:\t");
PreOrderTraverse_Recursive(tree);
printf("\n先序遍歷--非遞迴實現:\t");
PreOrderTraverse_NoRecursive(tree);
/***** 中序遍歷 *****/
printf("\n\n中序遍歷--遞迴實現:\t");
INOrderTraverse_Recursive(tree);
printf("\n中序遍歷--非遞迴實現:\t");
INOrderTraverse_NoRecursive(tree);
/***** 後序遍歷 *****/
printf("\n\n後序遍歷--遞迴實現:\t");
PostOrderTraverse_Recursive(tree);
printf("\n後序遍歷--非遞迴實現:\t");
PostOrderTraverse_NoRecursive(tree);
/***** 層次遍歷 *****/
printf("\n\n層次遍歷: ");
OrderTraverse(tree);
return 0;
}
四、思考
上面完整程式碼的實現過程中留了個思考:即在建立一棵二叉樹的時候,create函式為什麼入參的時候,形參用的是二級指標**T,底下使用的是*T(其實**T的*T就是個一級指標)?既然用的時候是*T本質就是個一級指標,為什麼不直接傳入個一級指標*T,然後使用的時候直接T呢?
要弄明白這個問題,首先來看一段程式碼,下面程式碼想實現的功能是,main函式裡的變數a尚未賦值,呼叫create函式,給main函式裡的變數a賦個值:
// code1
#include "stdio.h"
void create(int x)
{
x = 1;
return;
}
int main()
{
int a;
create(a);
return 0;
}
根本都不用編譯執行去驗證,單用眼看就知道根本不行(原因懶得解釋)。那如何實現這個功能呢?有兩種方案可以實現:
1、返回值形式
// code2
#include "stdio.h"
int create(int x)
{
x = 1;
return x;
}
int main()
{
int a;
a = create(a);
return 0;
}
2、利用指標
// code3
#include "stdio.h"
void create(int *x)
{
*x = 1;
return;
}
int main()
{
int a;
create(&a);
return 0;
}
有的人看了第2種方法之後會想,好,你這裡把main裡的棧變數a的地址傳到create裡,通過在create裡直接對a地址進行修改是可行的。那建立二叉樹的時候,也直接在main裡面定義一個二叉樹,然後將該樹的地址傳給create函式,傳入一級指標,直接在create裡對數的地址進行操作,不也可以達到修改main裡的樹狀態嗎,這樣不就可以直接在create裡形參傳一級指標了嗎?
是的,沒錯,這樣當然可行,不管有多少個函式,只要是對同一個地址上進行操作,理論上都是可以修改該地址上儲存的值的。注意我上面說的,是同一個地址。但問題是,在create函式裡我們使用了malloc;當在main裡面定義了Tree之後,系統就為這個Tree分配了一個地址,而malloc之後系統又會分配另一塊地址,這兩個地址絕非是同一塊地址。因此create裡對malloc的那塊地址的各種操作,並不會改變main裡面的那塊地址上的值。如下程式碼,並不能實現修改a的值:
// code4
#include "stdio.h"
#include "stdlib.h"
void create(int *x)
{
x = (int *)malloc(sizeof(int));
*x = 1;
return;
}
int main()
{
int a;
create(&a);
return 0;
}
但,是不是malloc就一定不可以?當然不是,我們依舊可以採取有返回值的方式,將這塊地址返回給main:
// code5
#include "stdio.h"
#include "stdlib.h"
int *create(int *x)
{
x = (int *)malloc(sizeof(int));
*x = 1;
return x;
}
int main()
{
int *a;
a = create(a);
return 0;
}
那又有人問了,我現在就不想帶返回值,我就想定義void型別,完了我還就想用create裡形參用的是一級指標,那行不行呢?也當然可以:
// code6
#include "stdio.h"
#include "stdlib.h"
void create(int *x)
{
*x = 1;
return;
}
int main()
{
int *a;
a = (int *)malloc(sizeof(int));
create(a);
return 0;
}
或許又有人說了,你為什麼一定要用malloc函式,能不能不要用?或者是,我不想在main裡面提前malloc好地址、create函式我就想定義為void型、形參我就想用一級指標,那行不行呢?就比如code3裡,main裡面並沒有malloc、create是void、形參是一級指標。
那可行不可行呢?當然可行!因為樹採用鏈式儲存結構時,其本質就是一個連結串列。對連結串列而言每個結點本質都是一個指標,當一個指標指向不明確時,是禁止被操作(儲存資料)的。
// code7
#include "stdio.h"
#include "stdlib.h"
typedef struct tagNode {
int data;
struct tagNode *next;
} Node;
void create(Node *h)
{
h->data = 1; // 沒問題。並且會同步修改main裡的head->data值
Node *node; // 宣告一個node結點
node->data = 2; // 給node儲存資料。錯誤,node本質是一個指標,其指向並不明確,野指標無法被賦值。
node->next = NULL;
h->next = node; // h連結上node。
return;
}
int main()
{
Node head;
create(&head);
return 0;
}
上述程式碼顯然有問題,node本質就是個指標,並且還是個野指標,野指標禁止參與操作,以上程式碼和下面的程式碼無異,都是對野指標進行了操作:
// code8
#include "stdio.h"
int main()
{
int *p;
*p = 1; // 錯誤。野指標禁止參與運算
return 0;
}
所以對於每個node結點,必須malloc一塊地址,使node有明確指向:
// code9
#include "stdio.h"
#include "stdlib.h"
typedef struct tagNode {
int data;
struct tagNode *next;
} Node;
void create(Node *h)
{
h->data = 1; // 沒問題。並且會同步修改main裡的head->data值
Node *node = (Node *)malloc(sizeof(Node)); // 宣告一個node結點
node->data = 2; // 給node儲存資料。
node->next = NULL;
h->next = node; // h連結上node。
return;
}
int main()
{
Node head;
create(&head);
printf("%d", (&head)->next->data);
return 0;
}
上述code9程式碼就沒有任何問題。聰明的人可能已經發現了,main裡面的head沒有malloc、create是void、create形參是一級指標。同理,對於二叉樹Tree的建立而言,當然也可以這樣去操作,但注意在create裡面就不能再對Tree的根進行malloc了,否則會導致main裡面的Tree和Create裡的Tree,不是一個Tree。
而現在是希望在create裡面給Tree分配個記憶體、並且又希望這個記憶體地址和main裡面的Tree保持一致、並且又不想以返回值的形式把malloc的這塊地址給返回出去,那就只能在create的形參裡使用二級指標。
所以以上解釋了一大堆的同時也可以看出,一棵二叉樹建立是有多種方式是可行的。形參具體怎麼傳值,依賴於你的實現方式。比如,我這裡的create的完全可以使用一級指標去傳參定義:
#include "stdio.h"
#include "stdlib.h"
/* 二叉樹結構定義 */
typedef struct tagBinaryTree {
int data;
struct tagBinaryTree *lchild;
struct tagBinaryTree *rchild;
} BiTree;
/* 建立二叉樹 */
void CreateBiTree(BiTree *T)
{
/* 根結點 */
T->data = 3;
/* 根的左孩子 */
T->lchild = (BiTree *)malloc(sizeof(BiTree));
T->lchild->data = 5;
/* 根的右孩子 */
T->rchild = (BiTree *)malloc(sizeof(BiTree));
T->rchild->data = 7;
/* 根的左孩子的左孩子 */
T->lchild->lchild = (BiTree *)malloc(sizeof(BiTree));
T->lchild->lchild->data = 1;
T->lchild->lchild->lchild = NULL;
T->lchild->lchild->rchild = NULL;
/* 根的左孩子的右孩子 */
T->lchild->rchild = (BiTree *)malloc(sizeof(BiTree));
T->lchild->rchild->data = 4;
T->lchild->rchild->lchild = NULL;
T->lchild->rchild->rchild = NULL;
/* 根的右孩子的左孩子 */
T->rchild->lchild = (BiTree *)malloc(sizeof(BiTree));
T->rchild->lchild->data = 2;
T->rchild->lchild->lchild = NULL;
T->rchild->lchild->rchild = NULL;
/* 跟的右孩子的右孩子 */
T->rchild->rchild = (BiTree *)malloc(sizeof(BiTree));
T->rchild->rchild->data = 6;
T->rchild->rchild->lchild = NULL;
T->rchild->rchild->rchild = NULL;
return;
}
int main()
{
BiTree tree;
CreateBiTree(&tree); /* 建立樹 */
return 0;
}
五、後記
另外,因為樹型結構本身就是一種遞迴結構,所以在後續有關樹的各種操作中,都直接用遞迴方式了,就不再思考非遞迴方式是如何實現的。