[資料結構]二叉樹的前中後序遍歷(遞迴+迭代實現)

Amαdeus發表於2023-01-30

二叉樹的遍歷

主要的三種遍歷方式

二叉樹主要的遍歷方式有前序遍歷、中序遍歷後序遍歷
(1)前序遍歷:根節點-->左子樹-->右子樹
(2)中序遍歷:左子樹-->根節點-->右子樹
(3)後序遍歷:左子樹-->右子樹-->根節點

其實還有一種比較基礎的遍歷方式是層次遍歷,但是在本篇文章中不會涉及層次遍歷的內容。

兩種基礎的實現方法

遞迴

以前序遍歷為例,按照根節點-->左子樹-->右子樹的順序遍歷。先訪問根節點,然後再分別對當前根節點的左子樹和右子樹重複同樣的操作。中序遍歷和後序遍歷也都是類似的,由此可以看出二叉樹遍歷本身就具有遞迴的性質。

迭代

我們也可以用迭代的方式來實現遞迴的過程,遞迴本質上隱式地維護了一個棧,所以可以用這個結構來模擬整個二叉樹遍歷的過程。



前序遍歷

前序遍歷(遞迴)

遞迴實現前序遍歷思路

我們只需要寫一個遞迴函式來實現前序遍歷。先考慮下遞迴函式停止遞迴的邊界條件,很明顯在遇到空指標NULL時,我們需要停止遞迴操作,此時直接return即可。
結合前序遍歷的遍歷順序和上文中遞迴方法的圖解,遞迴函式只需要先執行列印根節點資料的操作,然後再依次執行遞迴訪問左子樹、遞迴訪問右子樹就可以了。

假設遞迴函式名為 preorder,則順序為:
(1)print(root->data)
(2)preorder(root->left)
(3)preorder(root->right)

遞迴實現前序遍歷圖解

以下為二叉樹前序遍歷例項的遞迴版圖解

(1)

(2)

(3)

(4)

(5)

(6)

(7)

(8)

為了方便快速手寫出二叉樹前序遍歷,我們可以形象地把前序遍歷看作是一個小人以根節點為起點沿著二叉樹的外圍跑一圈,小人依次經過的節點就是前序遍歷的結果。(重複經過的節點不再放入前序遍歷結果序列中)

遞迴實現前序遍歷程式碼

//前序 遞迴遍歷二叉樹
void Show_Pre_Order(BinaryTree root){
    if(root == NULL)
    	return;
    printf("%c ", root->data);
    Show_Pre_Order(root->leftchild);
    Show_Pre_Order(root->rightchild);
}

前序遍歷(迭代)

迭代實現前序遍歷思路

我們使用棧來模擬遞迴的過程,先定義一個遍歷指標 t 並指向 root,棧用來儲存當前遍歷到的節點,整個過程用一個while迴圈來進行迭代。大致過程就是先列印當前節點資料,再一直往左走,邊走邊列印節點資料,同時將當前節點入棧,一直遍歷到最左邊的節點為止;然後取出棧頂節點,訪問其右子樹,再重複上述操作。
大致操作如下:
(1)列印節點資料;
(2)一直向左遍歷,將節點入棧,並重復步驟(1),直到到達最左邊;
(2)取棧頂節點,前往右子樹,重複上述操作。

我們再來考慮停止迭代的邊界條件,當 t 不為空指標NULL時,很顯然需要訪問當前 t 指向的節點;那麼如果當前 t 為NULL,難道就可以退出迴圈嗎?答案顯然是否定的,例如當我們遍歷到二叉樹的某個葉子節點時,已經到達當前子樹的最左邊,轉而前往右邊,而此時右邊也是NULL,那麼此時 t 變為NULL,但是棧中還有節點未彈出,整個二叉樹的前序遍歷還沒有完成。所以當棧不為空時,也要繼續遍歷。綜上所述,停止迭代的邊界條件為 t == NULL 並且 stack為空

迭代實現前序遍歷圖解

(1)

(2)

(3)

(4)

(5)

(6)

(7)

(8)

(9)

(10)

(11)

(12)

(13)

(14)

(15)

迭代實現前序遍歷程式碼

//利用棧前序遍歷   STL stack
void Show_Pre_OrderII(BinaryTree root){
    stack<BinaryTree> s;
    BinaryTree t = root;
    while(t || !s.empty()){
        while(t){
	    printf("%c ", t->data);
	    s.push(t);
	    t = t->leftchild;	 //一直往左邊走
        }
        t = s.top();
        s.pop();
        t = t->rightchild;	
    }
}


中序遍歷

中序遍歷(遞迴)

遞迴實現中序遍歷思路

寫一個遞迴函式,遞迴結束的邊界條件很簡單,也是遇到空指標NULL。
按照中序遍歷的順序,先遞迴訪問左子樹,再列印根節點資料,最後再遞迴訪問右子樹即可。

假設遞迴函式名為 infixorder,則順序為:
(1)infixorder(root->left)
(2)print(root->data)
(3)infixorder(root->right)

遞迴實現中序遍歷圖解

(1)

(2)

(3)

(4)

(5)

(6)

(7)

(8)

(9)

(10)

中序遍歷的結果同樣也可以用一種方式來快速寫出,將每個節點垂直下降至統一平面,得到的序列就是中序遍歷的結果。

遞迴實現中序遍歷程式碼

//中序 遞迴遍歷二叉樹
void Show_Infix_Order(BinaryTree root){
    if(root == NULL)
        return;
    Show_Infix_Order(root->leftchild);
    printf("%c ", root->data);
    Show_Infix_Order(root->rightchild);
}

中序遍歷(迭代)

迭代實現中序遍歷思路

同樣使用棧來模擬遞迴的過程,先定義一個遍歷指標 t 指向 root,棧用來儲存當前遍歷到的節點。大致過程就是先一直往左走,同時將當前節點入棧,一直遍歷到最左邊的節點為止;然後取出棧頂節點,列印節點資料,訪問其右子樹,再重複上述操作。可以看出迭代版中序遍歷和前序遍歷的思路只在列印資料的地方有一點差別。

大致操作如下:
(1)一直向左遍歷,將節點入棧,直到到達最左邊;
(2)取棧頂節點,列印節點資料;
(2)前往右子樹,重複上述操作。

整個迭代的過程和前序遍歷大致一致,只有列印資料位置不同,停止迭代的條件和前序遍歷一樣,也是 t == NULL 並且 stack為空

迭代實現中序遍歷圖解

(1)

(2)

(3)

(4)

(5)

(6)

(7)

(8)

(9)

(10)

(11)

(12)

(13)

(14)

(15)

迭代實現中序遍歷程式碼

//利用棧中序遍歷   STL stack
void Show_Infix_OrderII(BinaryTree root){
    stack<BinaryTree> s;
    BinaryTree t = root;
    while(t || !s.empty()){
        while(t){
            s.push(t);
            t = t->leftchild;   //一直往左邊走
        }
        t = s.top();	
        s.pop();  
        printf("%c ", t->data);         
        t = t->rightchild;       
    }
}


後序遍歷

後序遍歷(遞迴)

遞迴實現後序遍歷思路

寫一個遞迴函式,遇到空指標NULL結束遞迴。
按照後序遍歷的順序,先遞迴訪問左子樹,再遞迴訪問右子樹,最後列印根節點資料。

假設遞迴函式名為 postorder,則順序為:
(1)postorder(root->left)
(2)postorder(root->right)
(3)print(root->data)

遞迴實現後序遍歷圖解

(1)

(2)

(3)

(4)

(5)

(6)

(7)

(8)

(9)

(10)

(11)

為了方便快速寫出後序遍歷的結果,可以將後序遍歷看做是剪葡萄一樣,將節點一個一個剪下。

遞迴實現後序遍歷程式碼

//後序 遞迴遍歷二叉樹
void Show_Post_Order(BinaryTree root){
    if(root == NULL)
        return;
    Show_Post_Order(root->leftchild);
    Show_Post_Order(root->rightchild);
    printf("%c ", root->data);
}

後序遍歷(迭代)

迭代實現後序遍歷思路

後序遍歷的迭代寫法比前面的兩種要稍微複雜一點,同樣使用棧來模擬遞迴的過程,先定義一個遍歷指標 t 指向 root,棧用來儲存當前遍歷到的節點。
同時還需要定義一個指標 last 標記上一次訪問過的節點。
我們依舊需要先一直往左走,並且將走過的節點入棧,直到到達最左邊。然後取出當前棧頂節點,討論是否訪問當前出棧節點。後序遍歷的順序是左子樹-->右子樹-->根節點,其實討論的就是當前出棧的這個子樹根節點是否能夠被訪問。噹噹前出棧節點的右子樹為NULL時,則當前出棧節點可以訪問,那麼僅此一種情況嗎?並不是,假如某個出棧子樹的根節點,存在右子樹,但是這個右子樹已經遍歷完畢了,那麼也沒有再訪問右子樹的需要,此時同樣可以訪問出棧節點。滿足這個條件只會在左子樹已經訪問過並且右子樹根節點為上一個訪問過的節點情況下出現,所以我們只需要用一個 last 來記錄當前訪問過的節點就行了,為了防止出現死迴圈,還需要將 t 置NULL。如果這兩個條件都不滿足,那麼當前出棧節點不能訪問,需要重新入棧,並且轉而訪問右子樹。

大致操作如下:
(1)一直向左遍歷,將節點入棧,直到到達最左邊;
(2)取棧頂節點,討論是否可以訪問當前出棧節點;
(3)如果 t == NULL 或者 t == last,則可以訪問,並用 last 記錄當前訪問的節點,將 t 置NULL;
否則不能訪問,將當前出棧節點重新入棧,轉而前往右子樹。

停止迭代的條件和前序遍歷、中序遍歷一樣,也是 t == NULL 並且 stack為空

迭代實現後序遍歷圖解

(1)

(2)

(3)

(4)

(5)

(6)

(7)

(8)

(9)

(10)

(11)

(12)

(13)

(14)

(15)

(16)

(17)

(18)

迭代實現後序遍歷程式碼

//利用棧後序遍歷   STL stack
void Show_Post_OrderII(BinaryTree root){
    stack<BinaryTree> s;
    BinaryTree t = root, last;
    while(t || !s.empty()){
        while(t){
            s.push(t);
            t = t->leftchild;    //先到達最左側節點
        }
        t = s.top();
        s.pop();
        //如果當前節點無右子樹或者右子樹根節點為上一個訪問過的節點
        if(!t->rightchild || t->rightchild == last){
            printf("%c ", t->data);
            last = t;            //記錄當前訪問過的節點
            t = NULL;
        }else{
            s.push(t);           //否則將當前節點重新入棧
            t = t->rightchild;   //轉而前往其右子樹
        }
    }
}


程式測試

完整程式程式碼

#include<stdio.h>
#include<stdlib.h>
#include<stack>
#include<iostream>
using namespace std;

typedef char Elemtype;
typedef struct BiNode{

    Elemtype data;
    struct BiNode *leftchild;       //左兒子
    struct BiNode *rightchild;      //右兒子

}Node, *BinaryTree;

//遞迴初始化二叉樹 賦值二叉樹
BinaryTree Create_BinaryTree(){
    Elemtype data;
    BinaryTree T;
    scanf("%c", &data);                     //輸入節點資料
    getchar();

    if(data == '#')                         //輸入#停止建立子樹
        return NULL;  
    T = (BinaryTree)malloc(sizeof(Node));
    T->data = data;

    printf("輸入 %c 的左子樹資料: ", data);  //遞迴建立左子樹
    T->leftchild = Create_BinaryTree();
    printf("輸入 %c 的右子樹資料: ", data);  //遞迴建立右子樹
    T->rightchild = Create_BinaryTree();

    return T;
}

//前序 遞迴遍歷二叉樹
void Show_Pre_Order(BinaryTree root){
    if(root == NULL)
    	return;
    printf("%c ", root->data);
    Show_Pre_Order(root->leftchild);
    Show_Pre_Order(root->rightchild);
}

//中序 遞迴遍歷二叉樹
void Show_Infix_Order(BinaryTree root){
    if(root == NULL)
        return;
    Show_Infix_Order(root->leftchild);
    printf("%c ", root->data);
    Show_Infix_Order(root->rightchild);
}

//後序 遞迴遍歷二叉樹
void Show_Post_Order(BinaryTree root){
    if(root == NULL)
        return;
    Show_Post_Order(root->leftchild);
    Show_Post_Order(root->rightchild);
    printf("%c ", root->data);
}

//利用棧前序遍歷   STL stack
void Show_Pre_OrderII(BinaryTree root){
    stack<BinaryTree> s;
    BinaryTree t = root;
    while(t || !s.empty()){
        while(t){
	    printf("%c ", t->data);
	    s.push(t);
	    t = t->leftchild;	 //一直往左邊走
        }
        t = s.top();
        s.pop();
        t = t->rightchild;	
    }
}

//利用棧中序遍歷   STL stack
void Show_Infix_OrderII(BinaryTree root){
    stack<BinaryTree> s;
    BinaryTree t = root;
    while(t || !s.empty()){
        while(t){
            s.push(t);
            t = t->leftchild;   //一直往左邊走
        }
        t = s.top();	
        s.pop();  
        printf("%c ", t->data);         
        t = t->rightchild;       
    }
}

//利用棧後序遍歷   STL stack
void Show_Post_OrderII(BinaryTree root){
    stack<BinaryTree> s;
    BinaryTree t = root, last;
    while(t || !s.empty()){
        while(t){
            s.push(t);
            t = t->leftchild;    //先到達最左側節點
        }
        t = s.top();
        s.pop();
        //如果當前節點無右子樹或者右子樹根節點為上一個訪問過的節點
        if(!t->rightchild || t->rightchild == last){
            printf("%c ", t->data);
            last = t;            //記錄當前訪問過的節點
            t = NULL;
        }else{
            s.push(t);           //否則將當前節點重新入棧
            t = t->rightchild;   //轉而前往其右子樹
        }
    }
}

int main(){
    BinaryTree T;
    printf("輸入二叉樹根節點資料: ");
    T = Create_BinaryTree();

    printf("\n先序遍歷二叉樹:\n");
    Show_Pre_Order(T);
    printf("\n");

    printf("\n中序遍歷二叉樹:\n");
    Show_Infix_Order(T);
    printf("\n");

    printf("\n後序遍歷二叉樹:\n");
    Show_Post_Order(T);
    printf("\n");

    printf("\n利用棧非遞迴前序遍歷二叉樹:\n");
    Show_Pre_OrderII(T);
    printf("\n");

    printf("\n利用棧非遞迴中序遍歷二叉樹:\n");
    Show_Infix_OrderII(T);
    printf("\n");

    printf("\n利用棧非遞迴後序遍歷二叉樹:\n");
    Show_Post_OrderII(T);
    printf("\n");
}

程式執行測試結果

相關文章