數據結構(十三) -- C語言版 -- 樹 - 二叉樹的遍歷(遞歸、非遞歸)

零、讀前說明

  • 本文中所有設計的代碼均通過測試,並且在功能性方面均實現應有的功能。
  • 設計的代碼並非全部公開,部分無關緊要代碼並沒有貼出來。
  • 如果你也對此感興趣、也想測試源碼的話,可以私聊我,非常歡迎一起探討學習。
  • 由於時間、水平、精力有限,文中難免會出現不準確、甚至錯誤的地方,也很歡迎大佬看見的話批評指正。
  • 嘻嘻。。。。 。。。。。。。。收!

   二叉樹是一種非線性的數據結構,在懟他進行操作時,總是需要逐一對每個元素進行操作,這樣就存在一個操作的順序的問題,由此提出二叉樹的遍歷的操作。

   所謂二叉樹的遍歷就是按照一定的順序訪問二叉樹的每一個節點一次且僅一次的過程,這裏的訪問可以是輸出、比較、更新、查看元素內容等等操作。

一、遞歸遍歷

   遍歷指的是:從根節點出發,按照某種次序依次訪問二叉樹中的所有節點,使每個節點被訪問且僅被訪問一次。

  如果限定先左後右,那就有三種方法:

   DLR – 先序遍歷 :即先遍歷根節點、再遍歷左子樹、再遍歷右子樹
   LDR – 中序遍歷 :即遍歷先左子樹、再遍歷根節點、再遍歷右子樹
   LRD – 後序遍歷 :即遍歷先左子樹、再遍歷右子樹、再遍歷根節點
  還有另外一種常用的遍歷的方法,
   LBL – 層序遍歷 : 即先從上到下,從左到右一層一層進行遍歷

   注: LBL 爲博主自己爲了在視圖上和上面的統一對齊所以自己定義的名稱,意取英文(layer by levelTM的縮寫而成,希望不要過分關注並吐槽。嘻嘻嘻。。。

1.1、先序遍歷

   先序遍歷:根 -> 左 -> 右
   首先,用下面的一個簡單的二叉樹來表示先序遍歷的節點遍歷順序。
  
在這裏插入圖片描述

圖1.1 先序遍歷的節點遍歷順序表示圖

  
  首先從根節點 A 開始出發

  1、將根節點的數據輸出
  2、判斷是否存在左孩子
    1)如果存在則將左孩子輸出,並且以左孩子爲根節點再開始往下遍歷
      ①將此節點的數據輸出
      ②如果存在則將左孩子輸出,並且以左孩子爲根節點再開始往下遍歷
      … …
      ③如果不存在左孩子,則判斷並遍歷右孩子
      … …
    2)如果不存在左孩子,則判斷並遍歷右孩子
      ①將此節點的數據輸出
      ②如果存在則將左孩子輸出,並且以左孩子爲根節點再開始往下遍歷
      … …
      ③如果不存在左孩子,則判斷並遍歷右孩子
       … …
  3、判斷是否存在右孩子
    1)如果存在則將左孩子輸出,並且以左孩子爲根節點再開始往下遍歷
      ①將此節點的數據輸出
      ②如果存在則將左孩子輸出,並且以左孩子爲根節點再開始往下遍歷
      … …
      ③如果不存在左孩子,則判斷並遍歷右孩子
      … …
    2)如果不存在左孩子,則判斷並遍歷右孩子
      ①將此節點的數據輸出
      ②如果存在則將左孩子輸出,並且以左孩子爲根節點再開始往下遍歷
      … …
      ③如果不存在左孩子,則判斷並遍歷右孩子
      … …
  
  是的,上面這麼說明的過程就像一個套娃的模型,一個接一個,一層套一層。。。所以,對於上面圖1.1中的這個二叉樹的其先序遍歷的結果爲:

A->B->D->H->I->E->C->F->G

  
   那麼,綜合所述,其遍歷的代碼可以這樣編了。

/**
 * 功 能:
 *      二叉樹的遞歸遍歷 - 先序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void Traversal_Before(BiTNode *root)
{
    if (root == NULL) return;
    // 遍歷根節點
    printf("%c ", root->data);
    // 遍歷左子樹
    Traversal_Before(root->lchild);
    // 遍歷右子樹
    Traversal_Before(root->rchild);
}

   所以,其遍歷的效果輸出如下圖所示咯。

在這裏插入圖片描述

圖1.2 先序遍歷的效果圖

  

1.2、中序遍歷

  中序遍歷:左 -> 根 -> 右

   首先,用下面的一個簡單的二叉樹來表示中序遍歷的節點遍歷順序。
  
在這裏插入圖片描述

圖1.3 中序遍歷的節點遍歷順序表示圖

  
  首先從根節點 A 開始出發

  1、判斷是否存在左孩子
    1)如果存在則將繼續判斷左孩子是否也存在左孩子
      ①如果存在將繼續判斷左孩子是否也存在左孩子
      … …
      ②如果不存在左孩子,則將本節點輸出,並且回退到其父節點
      ③判斷是否存在右孩子
      … …
    2)如果不存在左孩子,則將本節點輸出
    3)判斷是否存在右孩子
      ①如果存在右孩子,則判斷右孩子是否存在左孩子
      … …
      ②如果不存在右孩子,則返回
  2、將節點的數據輸出
  3、判斷是否存在右孩子
    1)如果存在右孩子
      ①如果存在右孩子,則判斷右孩子是否存在左孩子
      … …
    2)如果不存在右孩子,則返回

  是的,上面這麼說明的過程就像一個套娃的模型,一個接一個,一層套一層。。。所以,對於上面圖1.2中的這個二叉樹的其中序遍歷的結果爲:

H->D->I->B->E->A->F->C->G

  
   那麼,綜合所述,其遍歷的代碼可以這樣編了。

/**
 * 功 能:
 *      二叉樹的遞歸遍歷 - 中序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void Traversal_Middle(BiTNode *root)
{
    if (root == NULL) return;

    // 遍歷左子樹
    Traversal_Middle(root->lchild);
    // 遍歷根節點
    printf("%c ", root->data);
    // 遍歷右子樹
    Traversal_Middle(root->rchild);
}

   所以,其遍歷的效果輸出如下圖所示咯。

在這裏插入圖片描述

圖1.4 中序遍歷的效果圖

  

1.3、後序遍歷

  後序遍歷:左 -> 右 -> 根

   首先,用下面的一個簡單的二叉樹來表示後序遍歷的節點遍歷順序。
  

在這裏插入圖片描述

圖1.5 後序遍歷的節點遍歷順序表示圖

  
  首先從根節點 A 開始出發

  1、判斷是否存在左孩子
    1)如果存在則將繼續判斷左孩子是否也存在左孩子
      ①如果存在將繼續判斷左孩子是否也存在左孩子
      … …
      ②如果不存在左孩子,則判斷是否存在右孩子
      … …
      ③將本節點數據輸出
      … …
    2)判斷是否存在右孩子
      ①如果存在右孩子,則判斷右孩子是否存在左孩子
      … …
      ②如果不存在右孩子,則將本節點輸出
    3)將本節點數據輸出
  2、判斷是否存在右孩子
    1)如果存在右孩子,則判斷此右孩子是否存在左孩子
      ①如果存在左孩子,則判斷此左孩子是否也存在左孩子
      … …
      ②如果不存在左孩子,則判斷此左孩子是否還存在右孩子
      … …
      ③如果不存在左右孩子,則將本節點輸出
    2)將本節點數據輸出
  3、將節點的數據輸出

  所以,對於上面圖1.3中的這個二叉樹的其後序遍歷的結果爲:

H->I->D->E->B->F->G->C->A

  
   那麼,綜合所述,其遍歷的代碼可以這樣編了。

/**
 * 功 能:
 *      二叉樹的遞歸遍歷 - 後序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void Traversal_Later(BiTNode *root)
{
    if (root == NULL)
        return;

    // 遍歷左子樹
    Traversal_Later(root->lchild);
    // 遍歷右子樹
    Traversal_Later(root->rchild);
    // 遍歷根節點
    printf("%c ", root->data);
}

   所以,其遍歷的效果輸出如下圖所示咯。

在這裏插入圖片描述

圖1.6 後序遍歷的效果圖

  

1.4、層序遍歷

  層序遍歷:逐層遍歷,按照樹的廣度(寬度)來進行遍歷

   首先,用下面的一個簡單的二叉樹來表示後序遍歷的節點遍歷順序。
  
在這裏插入圖片描述

圖1.7 層序遍歷的節點遍歷順序表示圖

  
  既然是進行逐層遍歷,那麼首先需要知道的是到底是這個數有多少層?
    首先求出左子樹的高度,然後求出右子樹的深(高)度,然後誰的的深(高)度高,那麼在誰的高度的基礎上加1就是此樹的深(高)度了
  至於代碼的話那先在這個地方賣個關子。詳細代碼可以參考:https://blog.csdn.net/zhemingbuhao/article/details/105909106

  已知樹的深度,那麼
    1、首先從根節點 A 開始出發,此時根節點也是所謂的第一層,遍歷輸出
    2、然後以根節點的左孩子作爲新的根節點遍歷且遍歷一次,然後再以根節點的右孩子作爲新的根節點,遍歷且遍歷一次
    … …
    n、遍歷第n層,則等同於以n-1層爲根節點,遍歷且遍歷一次其左孩子,然後遍歷一次其右孩子

  那麼問題又來了,怎麼來保證它遍歷了且只遍歷一次呢?
  拿在上面的這個二叉樹中,以第三層的節點D爲例子說明,此時遍歷的層數n=3,而其根節點爲第二層的B,B的根節點爲根節點A(第一層),那麼倒着推過來,需要遍歷D節點就需要走兩次遞歸的流程(A,B),即此時第3層減去經過的兩個節點(A,B)等於1,而在開始遍歷的時候根節點A的層數也爲1,是不是可以推出來這樣一個結論:

要遍歷第n層的節點,那就遞歸的次數爲n-1。

  所以,這個就是保證遍歷且遍歷一次的保證的手段。
  綜上所述,對於上面圖1.4中的這個二叉樹的其層序遍歷的結果爲:

A->B->C->D->E->F->G->H->I

  
   那麼,綜合所述,其遍歷的代碼可以這樣編了。

/**
 * 功 能:
 *      二叉樹的遞歸遍歷 - 第i層的遍歷
 * 參 數:
 *      root:要遍歷的樹
 *      i   :要遍歷的層
 * 返回值:
 *      無 
 **/
void tree_Level(BiTNode *root, int i)
{
    if (root == NULL || i == 0) return;
    if (i == 1)
    {
        printf("%c ", root->data);
        return;
    }
    tree_Level(root->lchild, i - 1);
    tree_Level(root->rchild, i - 1);
}
/**
 * 功 能:
 *      二叉樹的遞歸遍歷 - 層序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void Traversal_Level(BiTNode *root)
{
    int i = 0;
    if (root == NULL) return;
    
    for (i = 1; i <= tree.Depth(root); i++)
        tree_Level(root, i);
}

   所以,其遍歷的效果輸出如下圖所示咯。

在這裏插入圖片描述

圖1.8 層序遍歷的效果圖

  

  說明
    二叉樹的遞歸遍歷 - 層序遍歷的代碼參考了大佬的博文,感謝!大佬的博客原來鏈接,詳情請自行觀摩。

二、非遞歸遍歷

  
  1、深度遍歷的運行過程是先進後出的,自然的方法是棧和遞歸,包含先序遍歷,中序遍歷,後續遍歷
  2、廣度遍歷的運行過程是先進先出的,自然的方法是隊列,包含層序遍歷
  

2.1、中序遍歷

2.1.1、數據模型分析

  對於中序遍歷,首先是一直訪問左子樹,直到左子樹不存在,那麼在訪問左子樹的過程中,會一直訪問左子樹的雙親節點,比如下面圖中這樣的情況。
  
在這裏插入圖片描述

圖2.1 中序遍歷的節點遍歷順序表示圖

  

  爲了找到最後一個左子樹,他翻山越嶺才千辛萬苦的到達,所以在找到 D 的過程中,先後依次經歷了 A -> B -> C 三個大山。那麼在整個中序遍歷的過程中,用一個 的模型來表示貌似正好。
  
  就像下面這樣進行,從根節點開始。

  1、開始遍歷,判斷A節點是否存在左子樹,如果存在左子樹,那麼就需要去處理這個左子樹(B),那麼對於存在左子樹的A節點來說,那麼只能先保存起來。
  2、B爲A的左子樹,那首先需要判斷的還是是否左子樹,如果存在左子樹,那麼就去處理這個左子樹(C),B節點也需要先保存起來
  3、C爲B的左子樹,同樣先去判斷是否存在左子樹,存在左子樹即去處理左子樹,然後將C節點保存起來
  4、D爲C的左子樹,同樣先去判斷是否存在左子樹,此時判斷D節點不存在左子樹,那麼也就是意味着此時最底層的左子樹被找到了。那麼此時D節點即爲第一個被輸出的節點。
  4、根據中序遍歷的規則,左子樹已經就範,那麼需要找到這個左子樹的雙親節點,哎,目前最後一次保存的那個節點不就是麼,那麼拎出來然後輸出就是了,也就是C節點。
  5、接下里就是這個雙親節點(C)的右子樹了,一查,就是E節點,並且E節點也沒有其他的左子樹什麼的,那麼就將E節點輸出。
  6、現在需要找C節點的雙親節點然後需要判斷是否存在右子樹等子孫,還記得麼,倒數第二次保存的節點不就是麼(B節點),經查沒有其他子孫,拎出來輸出完事
  7、然後找B節點的雙親節點然後需要判斷是否存在右子樹等子孫,第一次保存的節點不就是麼(A節點),經查也沒有其他子孫,拎出來輸出完事。

  所以遍歷的順序爲:

D->C->E->B->A

  遍歷過程中的棧的變化情況如下圖所示。棧在整個遍歷過程中是動態變化的,結合上面的步驟分析,可以知道在某一時刻棧中的元素就只想這樣。
  
在這裏插入圖片描述

圖2.2 中序遍歷的節點棧的表示圖

  

  綜上所述,所以整個遍歷的過程就是一個先進去的後出來,這不就是一個 “棧”的模型麼!!!

  下面就用棧的模型在進行一個比較典型的樹的中序遍歷。樹的結構圖如下圖所示。

  
在這裏插入圖片描述

圖2.3 樹的表示圖

  
  1、開始遍歷,指針指向A,遍歷開始
    A有左子樹,所以A入棧
  2、指針移動到A的左子樹B
     B沒有左子樹,所以B被訪問
  3、B有右子樹,所以指針指向右子樹C
    C有左子樹,C入棧
  4、指針移動到D
    D沒有左子樹,所以D訪問
    D沒有右子樹,那麼根據棧頂回退到C,此時棧頂出棧,指針回到C位置
  5、C沒有右子樹並且C已經被訪問,所以根據目前棧頂回退到AA被訪問

  此時,棧的變化的過程爲如下圖所示。
  
在這裏插入圖片描述

圖2.4 棧的變化的過程表示圖

  

  6、A有右子樹,指針指向右子樹E
    E沒有左子樹,E被訪問
  7、E有右子樹,指針指向了右子樹F
    F有左子樹,F入棧
  8、指針移動到左子樹G
    G有左子樹,G入棧
  9、指針移動到左子樹H
    H沒有左子樹,被訪問
    H沒有右子樹,根據棧頂回退到GG出棧
  10、指針移動到GG有右子樹I
  11、指針移動到I
    I沒有左子樹,被訪問
    I沒有右子樹,回退到棧頂F,並且訪問F
  12、F沒有右子樹,則需要回退,此時棧爲空,表示此時樹遍歷完成

  此時,棧的變化的過程爲如下圖所示。
  
在這裏插入圖片描述

圖2.5 棧的變化的過程表示圖

  
  綜上所述,遍歷的步驟可以總結爲:
  步驟一
    1、如果節點有左子樹,則該節點入棧
    2、如果沒有左子樹,則訪問該節點
  步驟二
    3、如果有右子樹,則重複步驟一
    4、如果節點沒有右子樹,則說明節點訪問完畢,根據棧頂指示進行回退、訪問棧頂節點、訪問右子樹然後重複步驟一
    5、如果棧爲空,則表示遍歷結束。

2.1.2、代碼實現

  由上面的分析可知,此時需要用棧來實現遍歷,所以,中序遍歷的流程可以簡單的用下面圖來表示。
  

在這裏插入圖片描述

圖2.6 中序遍歷的流程示意圖

  
  綜上所述,那麼中序遍歷的代碼可以這樣寫。

/**
 * 功 能:
 *      查找二叉樹的左子樹,直到左子樹爲空
 * 參 數:
 *      root:要操作的樹
 * 返回值:
 *      成功:最底層的左子樹,也是中序遍歷的第一個節點
 *      失敗:NULL 
 **/
BiTNode *checkLeftChild(BiTNode *root, LinkStack *stack)
{
    if (root == NULL)
        return NULL;

    while (root->lchild != NULL)
    {
        fLinkStack.push(stack, (LinkStackNode *)root);
        root = root->lchild;
    }

    return root;
}

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷 - 中序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void nonTraversal_Middle(BiTNode *root)
{
    if (root == NULL)
        return;

    // 創建一個棧
    LinkStack *stack = fLinkStack.create();

    // 一直往左走,判斷是否存在左孩子,找到中序遍歷的起點
    BiTNode *tree = checkLeftChild(root, stack);
    while (tree)
    {
        printf("%c ", tree->data);
        // 如果tree有右子樹,重複步驟一
        if (tree->rchild != NULL)
        {
            tree = checkLeftChild(tree->rchild, stack); // 右子樹中序遍歷的起點
        }
        // 如果tree沒右子樹,根據棧頂回退
        else if (fLinkStack.length(stack) > 0) // 如果棧不爲空
        {
            tree = fLinkStack.top(stack); // 或者可以使用 stack.pop(),看具體的實現
            fLinkStack.pop(stack);
        }
        else // 如果沒有右子樹,並且棧爲空
        {
            tree = NULL;
        }
    }

    fLinkStack.destroy(stack);
}

   所以,其遍歷的效果輸出如下圖所示咯。

在這裏插入圖片描述

圖2.7 非遞歸中序遍歷效果圖

2.2、先序遍歷

2.2.1、數據模型分析

  
  先序遍歷:根節點 –> 左孩子 -> 右孩子

  首先建立一個棧
  1、從根節點開始遍歷,當指針到達根結點時,打印根結點
  2、然後判斷根結點是否有左孩子和右孩子
    1)如果當前結點存在左孩子
      ①如果存在右孩子,則將右孩子入棧
      ②打印左孩子,並且將左孩子作爲新的根結點進行判斷
    2)如果當前結點沒有左孩子,則開始判斷是否存在右孩子,接下面3
  3、然後判斷根結點是否存在右孩子
    1)如果存在右孩子,將右孩子打印,同時將右孩子作爲新的根結點判斷。
    2)如果沒有右孩子,則打印左孩子,同時將左孩子作爲新的根結點判斷。
  4、如果當前結點既沒有左孩子也沒有右孩子
    1)則說明當前結點爲葉子結點,此時將從棧中出棧一個節點,將此節點作爲當前的根結點
      ①打印結點
      ②將當前結點同樣按上面1、2、3、4開始依次判斷
    2)直至當前結點的左右孩子都爲空,且棧爲空時,遍歷結束。

  其實總結一句話就是:

  從根節點開始,沿着左孩子依次訪問途中經過的根節點,同時將右孩子入棧,在遍歷訪問完左子樹後出棧得到右子樹作爲根節點,如此重複,直到棧空。則完成遍歷。

2.2.2、代碼實現

  由上面的分析可知,此時需要用棧來實現遍歷,所以,先序遍歷的流程可以簡單的用下面圖來表示。
  

在這裏插入圖片描述

圖2.7 先序遍歷的流程示意圖

  
  綜上所述,那麼先序遍歷的代碼可以這樣寫。

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷 - 先序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無
 **/
void nonTraversal_Before(BiTNode *root)
{
    if (root == NULL) return;

    LinkStack *stack = fLinkStack.create();

    while (root || fLinkStack.length(stack) > 0)
    {
        while (root)
        {
            printf("%c ", root->data);
            fLinkStack.push(stack, (LinkStackNode *)root);
            root = root->lchild;
        }
        root = fLinkStack.pop(stack);

        root = root->rchild;
    }

    fLinkStack.destroy(stack);
}

   所以,其遍歷的效果輸出如下圖所示咯。

在這裏插入圖片描述

圖2.8 非遞歸先序遍歷效果圖

  

2.3、後序遍歷

2.3.1、數據模型分析與概要

  
  後序遍歷:左孩子 –> 右孩子 -> 根節點
  
  從前面的遞歸遍歷中可以看出來,後序遍歷,和先序遍歷中序遍歷的區別只是對於節點訪問色順序的變化,所以,在使用到非遞歸遍歷的時候我們使用到的還是去實現後序遍歷的非遞歸實現
  但是,對於後序遍歷,是先訪問左、再訪問右子樹,然後才訪問根節點,在非遞歸算法中,途經但是不輸出的節點需要入棧,但是無論如何,遍歷的順序不能改變,所以在利用棧回退時,並不能確定是從左子樹回退到根節點,還是從右子樹回退到根節點的,如果從左子樹回退到根節點,此時就應該去訪問右子樹,而如果從右子樹回退到根節點,此時就應該訪問根節點。
  所以這也導致了樹的後續非遞歸遍歷的實現也有一定的難度,但是同樣的,難度增加意味着需要實現的步驟可能多,那麼同樣實現的方式也比較多樣了。所以,在這兒我大概採用幾種比較常見的方式來實現。

2.3.2、實現方法一

  首先採用一種比較簡單的實現方式,,其主要實現的過程爲:

  這種遍歷方法類似於前序遍歷,但是又不是先序遍歷,這種遍歷的大致思路就是:
  1、先將按照 根節點 -> 右孩子 -> 左孩子 的順序進行遍歷,然後將遍歷要輸出的節點保存起來
  2、然後將上面保存起來的數據進行翻轉,然後輸出即可實現後序遍歷的效果。

  那麼先看看所使用遞歸的方式來驗證一下這種遍歷的正確性吧。
  實現的代碼可以這樣寫。

/**
 * 功 能:
 *      二叉樹的遞歸遍歷 - 類似於先序遍歷
 *      遍歷的順序爲 : 根節點 -> 右子樹 -> 左子樹
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void Traversal_rrl(BiTNode *root) 
{
    if (root == NULL) return;

    // 遍歷根節點
    printf("%c ", root->data);
    // 遍歷右子樹
    Traversal_rrl(root->rchild);
    // 遍歷左子樹
    Traversal_rrl(root->lchild);
}

  編譯運行之後的效果如圖所示。
在這裏插入圖片描述

圖2.9 類先序遍歷驗證

  
  有上面的圖片可以看出來,這種遍歷的輸出順序正好和後序遍歷的輸出是相反的,那麼翻轉後就是後序遍歷的輸出了。
  綜上所述,使用非遞歸實現 根節點 -> 右子樹 -> 左子樹 的順序的代碼可以這樣寫。

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷,遍歷順序 : 根節點 -> 右子樹 -> 左子樹
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void postOrder_Root_Right_Left(BiTNode *root)
{
    if (root == NULL) return;

    // 創建一個棧
    LinkStack *stack = fLinkStack.create();
    fLinkStack.push(stack, root);
    while (fLinkStack.length(stack) > 0)
    {
        root = fLinkStack.pop(stack);
        if (root->lchild != NULL)
        {
            fLinkStack.push(stack, (LinkStackNode *)root->lchild);
        }
        if (root->rchild != NULL)
        {
            fLinkStack.push(stack, (LinkStackNode *)root->rchild);
        }
        printf("%c ", root->data);
    }
	// 銷燬棧
    fLinkStack.destroy(stack);
}

  完成上面的部分代碼,對於樹的後序遍歷只能是完成了一半,我們還需要進行數據的翻轉才能算是完成後序遍歷的操作,那麼對於數據的翻轉,相信大家已經很很多種方法可以實現,那麼在本文中,爲了照顧絕大多數人,採用兩種比較簡單的方式分別進行實現。

  一、將要遍歷輸出的節點的值先保存在數組中,然後將數組進行反向輸出即口。請注意:此處說的是盡心反向輸出,而不是將數組進行翻轉。

  所以代碼可以這樣寫咯。

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷 - 後序遍歷
 *          遍歷順序 : 根節點 -> 右子樹 -> 左子樹
 *          採用數組進行翻轉輸出
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void postorder_Root_Right_Left_Array(BiTNode *root)
{
    if (root == NULL)
        return;
    // 定義一個數組,用於保存遍歷過程中要輸出的節點的值
    unsigned char buf[512] = {0};
    // 定義一個變量,用於記錄樹的節點的個數
    int i = 0, cnt = 0;

    // 創建一個棧
    LinkStack *stack = fLinkStack.create();
    // 將根節點進行入棧
    fLinkStack.push(stack, root);
    // 判斷棧是否爲空
    while (fLinkStack.length(stack) > 0)
    {
        // 出棧
        root = fLinkStack.pop(stack);
        // 將出棧的節點作爲新的根節點,判斷出棧的節點是否存在左孩子或者右孩子
        if (root->lchild != NULL)
        {
            // 節點是存在左孩子或者右孩子,直接入棧
            fLinkStack.push(stack, (LinkStackNode *)root->lchild);
        }
        // 將出棧的節點作爲新的根節點,判斷出棧的節點是否存在左孩子或者右孩子
        if (root->rchild != NULL)
        {
            // 節點是存在左孩子或者右孩子,直接入棧
            fLinkStack.push(stack, (LinkStackNode *)root->rchild);
        }
        // 將原本要輸出的節點保存到數組中
        // printf("%c ", root->data);
        buf[cnt++] = root->data;
    }
    // 將數組進行反向輸出
    for (i = cnt; i > 0; i--)
        printf("%c ", buf[i - 1]);

	// 銷燬棧
    fLinkStack.destroy(stack);
}

  二、提到數據的翻轉或者反向,那麼棧的天然特性可以毫不費力的實現。

  所以代碼可以這樣寫咯。

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷 - 後序遍歷
 *          遍歷順序 : 根節點 -> 右子樹 -> 左子樹
 *          採用 棧 進行翻轉輸出
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void postOrder_Root_Right_Left_Stack(BiTNode *root)
{
    if (root == NULL)
        return;

    // 創建一個棧,用於遍歷過程中記錄節點
    LinkStack *stack1 = fLinkStack.create();
    // 在創建一個棧,用於在輸出的節點的翻轉
    LinkStack *stack2 = fLinkStack.create();

    // 將根節點進行入棧
    fLinkStack.push(stack1, root);
    // 判斷棧是否爲空
    while (fLinkStack.length(stack1) > 0)
    {
        // 出棧
        root = fLinkStack.pop(stack1);
        // 將出棧的節點作爲新的根節點,判斷出棧的節點是否存在左孩子或者右孩子
        if (root->lchild != NULL)
        {
            // 節點是存在左孩子或者右孩子,直接入棧
            fLinkStack.push(stack1, (LinkStackNode *)root->lchild);
        }
        // 將出棧的節點作爲新的根節點,判斷出棧的節點是否存在左孩子或者右孩子
        if (root->rchild != NULL)
        {
            // 節點是存在左孩子或者右孩子,直接入棧
            fLinkStack.push(stack1, (LinkStackNode *)root->rchild);
        }
        // 將原本要輸出的節點保存到 棧 中
        // printf("%c ", root->data);
        fLinkStack.push(stack2, root);
    }
    // 將棧中的節點輸出
    while (fLinkStack.length(stack2) > 0)
    {
        root = fLinkStack.pop(stack2);
        printf("%c ", root->data);
    }
	
	// 銷燬棧
    fLinkStack.destroy(stack1);
    fLinkStack.destroy(stack2);
}

2.3.3、實現方法二

  後序遍歷,其的主要的難點在於如何去判斷並且確定是 棧的回退 是從 左孩子 回退的還是 從右孩子 回退的用一種方法來去確定回退的根源,那麼剩下的遍歷過程與先序遍歷和中序遍歷相差不大了。所以相比前序和後序,後序遍歷必須要在壓棧時添加信息或者在出棧的時候去做相應的判斷
  下面的兩種實現方式都採用的是在元素出棧的時候保存節點,然後在下次遍歷的過程中的棧頂元素比較來確定當前需要去遍歷的是左子樹還是右子樹。其中主要的代碼實現如下所示。

  實現代碼一:

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷 - 後序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void nonTraversal_Post(BiTNode *root)
{
    if (root == NULL)
        return;
    // 創建一個棧
    LinkStack *stack = fLinkStack.create();
    BiTNode *top = NULL, *temp = NULL;

    // 節點存在並且棧不爲空
    while (root || fLinkStack.length(stack) > 0)
    {
        // 節點存在
        while (root) // 從當前節點開始判斷是否存在左孩子,一直到最後一個左孩子節點
        {
            // 將當前節點入棧
            fLinkStack.push(stack, (LinkStackNode *)root);
            // 將指針指向左孩子節點,然後判斷是否存在左孩子
            root = root->lchild;
        }

        // 左孩子不存在,此時獲取棧頂元素,根據棧頂元素進行下一步
        top = fLinkStack.top(stack);

        // 判斷是否存在右孩子
        // 或者棧頂元素的右孩子與出棧的元素相同
        if (top->rchild == NULL || top->rchild == temp)
        {
            // 如果不存在右孩子,則說明此節點爲葉子節點,將節點值打印
            printf("%c ", top->data);
            // 將此節點出棧,並記錄下來用作下次棧頂元素的右孩子的判斷
            // 用於判斷是從左孩子回退的還是右孩子回退的
            temp = fLinkStack.pop(stack);
        }
        else
        {
            // 將指針指向右孩子節點,然後重新開始判斷
            root = top->rchild;
        }
    }

	// 銷燬棧
    fLinkStack.destroy(stack);
}

  實現代碼二:

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷 - 後序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void nonTraversal_PostOrder(BiTNode *root)
{
    if (root == NULL)
        return;
    // 創建一個棧
    LinkStack *stack = fLinkStack.create();
    BiTNode *top = root, *temp = NULL;
    // 將根節點入棧
    fLinkStack.push(stack, root);
    // 判斷棧是否爲空
    while (fLinkStack.length(stack) > 0)
    {
        // 獲取棧頂元素,並判斷是否有左右孩子
        top = fLinkStack.top(stack);

        // 不存在左右孩子,表示此節點爲葉子節點
        // 根據棧頂元素與上次出棧的元素判斷來確定是從左或者右孩子回退的
        if ((top->lchild == NULL && top->rchild == NULL) ||
            ((temp == top->lchild || temp == top->rchild) && temp != NULL))
        {
            printf("%c ", top->data);
            // 出棧,並記錄下來用作下次棧頂元素的右孩子的判斷
            temp = fLinkStack.pop(stack);
        }
        else // 存在左右孩子,入棧
        {
        	if (top->lchild != NULL)
            {
                fLinkStack.push(stack, top->lchild);
            }
            if (top->rchild != NULL)
            {
                fLinkStack.push(stack, top->rchild);
            }
        }
    }
	
	// 銷燬棧
    fLinkStack.destroy(stack);
}

   綜上所述,所有的實現的方法的效果圖如下圖所示咯。

在這裏插入圖片描述

圖2.10 非遞歸後序遍歷效果圖

  

2.4、層序遍歷

2.4.1、數據模型分析

  層序遍歷:從根節點開始,逐層開始遍歷

  對於層序遍歷呢,和前面的幾種遍歷的方式不同點在於,層序層遍歷的時候需要從左到右依次去判斷此節點是否存在左孩子、右孩子,如果存在左孩子,那麼需要將左孩子存起來,然後再去判斷右孩子,如果也存在,那麼也需要存起來,這樣的話,可以完整的判斷出當前節點的左右孩子的情況,然後在遍歷輸出的時候需要先將左孩子輸出,再將右孩子輸出,這樣的話,從存儲到取出的順序來說的話,明顯就是一個先進先出的模型,所以採用隊列來進行層序遍歷的實現

  那麼,遍歷的思路可這樣。

  首先創建一個隊列
  1、從根節點開始遍歷,先將根節點入隊
  2、判斷此時隊列是否爲空
    1)如果隊列不爲空,說明此時節點存在
    2)將節點出隊列,並且將此節點輸出
    3)將此節點作爲新的根節點,判斷此節點是否存在左孩子、右孩子
      ①如果存在左孩子,則將左孩子入隊
      ②如果存在右孩子,則將右孩子入隊
    4)如果不存在左孩子右孩子,則返回判斷隊列是否爲空
  3、隊列爲空,則說明遍歷結束
  

2.4.2、代碼實現

  由上面的分析可知,此時需要用隊列來實現遍歷,所以,層序遍歷的流程可以簡單的用下面圖來表示。

  
在這裏插入圖片描述

圖2.11 層序遍歷的流程示意圖

  
  綜上所述,那麼層序遍歷的代碼可以這樣寫。

/**
 * 功 能:
 *      二叉樹的非遞歸遍歷 - 層序遍歷
 * 參 數:
 *      root:要遍歷的樹
 * 返回值:
 *      無 
 **/
void nonTraversal_Level(BiTNode *root)
{
    if (root == NULL)
        return;
    // 創建一個隊列
    LinkQueue *queue = fLinkQueue.create();
    // 將樹的根節點入隊列
    fLinkQueue.append(queue, (LinkQueueNode *)root);

    // 隊列不爲空
    while (fLinkQueue.length(queue) > 0)
    {
        // 出隊列,並且將出隊列的節點作爲根節點進行左右孩子的判斷
        root = fLinkQueue.subtract(queue);
        printf("%c ", root->data);

        // 判斷是否存在左孩子,存在直接將左孩子入隊列
        if (root->lchild != NULL)
        {
            fLinkQueue.append(queue, root->lchild);
        }
        // 判斷是否存在右孩子,存在直接將右孩子入隊列
        if (root->rchild != NULL)
        {
            fLinkQueue.append(queue, root->rchild);
        }
    }

    fLinkQueue.destroy(queue);
}

   所以,其遍歷的效果輸出如下圖所示咯。
在這裏插入圖片描述

圖2.12 非遞歸層序遍歷的效果圖

  

  本文總結了樹的幾種遍歷方法,分別是遞歸先序遍歷、遞歸中序遍歷、遞歸後續遍歷、遞歸層序遍歷、非遞歸先序遍歷、非遞歸中序遍歷、非遞歸後續遍歷、非遞歸層序遍歷,總共8中遍歷方式。所有的方式均已經完美實現,寫作不易,如果你覺得文章不錯或者對你有用,請動動你那發財的小手 點個贊 啦,當然 關注一波 那就更好啦,哈哈哈哈。。。。

  
在這裏插入圖片描述
  

上一篇:數據結構(十二) – C語言版 – 樹 - 二叉樹的創建與銷燬
下一篇:數據結構(十四) – C語言版 – 樹 - 二叉樹的葉子節點、深度、拷貝等

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章