數據結構(十七) -- C語言版 -- 樹 - 二叉樹的線索化及遍歷 -- 先序線索化、中序線索化、後序線索化

零、讀前說明

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

  既然你已經打開這篇了,那麼我還是想推薦再去看看這一篇:

    數據結構(十七) – C語言版 – 樹 - 二叉樹的線索化及遍歷 – 先序線索化、中序線索化、後序線索化

  上面這個系列已經用先序線索化的方式,說明了使用左指針域線索化鏈表線索化順序表線索化等等的方式。但是在結尾的時候也已經說明比較明顯的缺點:

  左指針域線索化會修改原本二叉樹的結構和邏輯關係 — 物是人非
  順序表線索化有內存空間的問題,並且插入刪除節點複雜 — 女孩的心思猜不到
  鏈表線索化爲單向鏈表,只能確定後繼 — 給不了Ta想要的幸福
  鏈表線索化爲雙向鏈表,佔地大 — 你知我深淺,我知你寬窄
  鏈表線索化爲雙向循環鏈表 ,佔地大— 手拉手的幸福甜死狗

  下面就開始說明一種將二叉樹線索化的常規、主流的方式。

一、線索化概述

  對於一個二叉樹來說,其二叉鏈表表示形式中正好有兩個指針域一個左子樹指針域,一個右子樹指針域。並且對於一個有 n 個節點的二叉鏈表, 每個節點有指向左右孩子的兩個指針域,所以共是 2n 個指針域。而 n 個節點的二叉樹一共有 n-1 條分支數,也就是說,其實是存在 2n-(n-1) = n+l 個空指針域。(摘自《大話數據結構》 P189 )。也就是說在上面的這個二叉樹中,有 7 個節點(ABCDEFG ),那麼就有8個空指針,將這些空指針利用起來(不同於前面提到的做指針域線索化),將其中一個指針用作前驅指針,另一個指針指向後繼指針,那麼這樣的話就不需要額外的空間了。

  我們還是以下面的這個二叉樹進行說明。

在這裏插入圖片描述

圖1.1 二叉樹、二叉鏈表表示示意圖

  
  我們已經知道了,二叉樹的遍歷有四種方式:先序遍歷、中序遍歷、後序遍歷、層序遍歷。

  那麼根據遍歷來進行線索化的方式也就有四種方式:先序線索化、中序線索化、後序線索化、層序線索化,其實嚴格意義上來說,除了遍歷的順序不同,其他的沒什麼太大的區別。對於線索化來將也是一樣的。

  所以,如果這個節點的左指針域是空的,那麼就讓其指向前驅,如果這個節點的右指針域爲空,那麼就讓其指向後繼,所以,四種線索化的示意圖可以表示爲下圖這樣。(其中 紅色線條 表示前驅、 藍色線條 表示爲後繼,均稱爲 線索。)

在這裏插入圖片描述

圖1.2 各種不同順序線索化的示意圖

  
  原節點中的爲空的指針域都用來表示 前驅和後繼 了,也就是除了 遍歷順序中第一個節點最後一個節點存在空指針域外,其餘的節點的空指針域已經用來保存線索了。

  從上面的示意圖中我麼可以看出來,每一個節點的左指針域和右指針域都都可以被使用,但是對於葉子節點GEF )或者存在一個空子樹的節點( DC )來說,其空指針域保存的是線索,但是對於其他的節點來說( AB ),不存在空指針域,而我們已知使用二叉鏈表表示的二叉樹的存儲結構爲下面的這樣的。

/* 二叉鏈表表示法的結構模型 */
typedef struct BiTNode
{
    TElemType data;         /* 節點數據 */
    struct BiTNode *lchild; /* 左孩子 */
    struct BiTNode *rchild; /* 右孩子 */
} BiTNode;

  那對於上面的描述,原來的二叉鏈表的節點中包含了兩個指針域(左右指針)、一個數據域,很難確定節點的兩個指針域保存的是子樹還是線索了。那麼爲了解決這樣的問題,我們需要在結構定義的基礎上加入兩個標誌位 lTag、rTag )分別用來表示當前指針的指向的含義。

  那麼加入標誌位之後其結構可以表示爲下圖這樣。
  
在這裏插入圖片描述

圖1.3 加入標誌位之後樹的數據結構

  
  其中, lTag、rTag 的標誌可以總結爲:
    當 lTag = 0 : lchild 指向節點的左孩子
    當 lTag = 1 : lchild 指向節點的前驅節點

    當 rTag = 0 : rchild 指向節點的右孩子
    當 rTag = 1 : rchild 指向節點的後繼節點

  所以將上面圖示中的節點的二叉樹的存儲結構表示爲下面的這樣的。

/* 加入線索標識二叉鏈表表示法的結構模型 */
typedef struct BiTNode
{
    TElemType data;         /* 節點數據 */
    struct BiTNode *lchild; /* 左孩子 */
    struct BiTNode *rchild; /* 右孩子 */
    int lTag;				/* 左孩子-線索標識 */
    int rTag;				/* 右孩子-線索標識 */
} BiTNode;

  那麼下面我們就開始按照先序線索化、中序線索化、後序線索化分別來進行說明表示。

二、中序線索化及其遍歷

  好,首先來說明一下中序線索化的過程及其遍歷吧。
  啊你說我爲什麼要先進行中序線索化而不是其他的?
  哈哈哈,當然是因爲愛情啊(*▔^▔*) 。。。。。
  好了,都三百斤的人了,穩重點好!這個主要是因爲呀,中序線索化比較簡單,而且線索化也比較完善。所以先說明中序線索化。然後在對比性的說明先序線索化和後序線索化

2.1、線索化過程說明

  開始中序線索化:在中序遍歷的基礎上進行線索化,
  首先在中序線索化之前,先定義兩個指針變量,用來輔助我們說明。

     current : 爲當前訪問的節點
     prev  : 爲之前訪問的最後一個節點,也就是 current 之前訪問的節點。

  對於中序遍歷,我們首先傳入輸的根節點,然後開始遍歷,那麼中序遍歷的第一個節點爲 D 節點。所以此時:

     current 指針指向節點 Dprev 指針爲 NULL ,如下圖所示。

在這裏插入圖片描述

圖2.1 中序線索化的示意圖1

  
  在當前節點 D 下,其左子樹指針域爲空,所以需要將其線索化爲前驅節點,但是節點 D 爲遍歷中的第一個節點,是沒有直接前驅的,所以,應該(圖中 翠綠色 表示)。

    current->lTag = 1;
    current->lchild = prev = NULL;

  根據中序遍歷,接下來應該是節點 D右子樹的判斷並遍歷,明顯節點 D 存在右子樹。所以訪問 G 節點。
  但是在訪問我們將 prev 指向 current 指針,讓 prev 指針跟上 current 指針(下圖中 天藍色虛線 表示)。
  
在這裏插入圖片描述

圖2.2 中序線索化的示意圖2

  
  繼續遍歷,將在當前指針 current 移動到第二個節點 G 下,其左子樹指針域爲空,所以需要將其線索化爲前驅節點,而其前驅節點爲 D ,正好保存在 prev 指針中,所以應該(下圖中 天藍色 表示):

    current->lTag = 1;
    current->lchild = prev;

  然後判斷 prev右子樹不爲空,不需要進行相關操作
  操作完成,我們將 prev 指向 current 指針,讓 prev 指針跟上 current 指針(圖中 草綠色 表示)。
  
在這裏插入圖片描述

圖2.3 中序線索化的示意圖3

  
  繼續遍歷,將在當前指針 current 移動到第三個節點 B 下,其左子樹指針域不爲空,所以不需要將其線索化。
  然後判斷 prev右子樹爲空,那麼需要進行線索化了。將此指針線索化成節點 G後繼節B ,此時(下圖中用 草綠色 表示)。

    prev->rTag = 1;
    prev->rchild = current;

  操作完成,我們將 prev 指向 current 指針,讓 prev 指針跟上 current 指針(圖中 橙色虛線 表示)。
  
在這裏插入圖片描述

圖2.4 中序線索化的示意圖4

  
  繼續遍歷,將當前指針 current 移動到下個節點 B 右子樹 E 下,節點 E 和節點 G 都是葉子節點,所以既需要線索化前驅也需要線索化後繼節點。

  首先來進行前驅線索化,那麼前驅線索化可以表示爲(下圖中 橙色 表示)。

    current->lTag = 1;
    current->lchild = prev;

  然後將 prev 指針跟上 current 指針,即可完成前驅線索化(用 紫色虛線 表示)。

  然後 current 指針移動到 A 節點,且節點 A左右子樹均存在所以也就不需要線索化了(用 紫色 表示),直接過去即可。

  由中序遍歷的順序, current 移動到 F 節點, F 節點爲葉子節點,所以需要線索化,根據前面的描述可以將節點 F左指針域線索化成其前驅節點A 節點(用 紫紅色 表示)。

  然後移動到 C 節點,進行 C 節點的線索化, prev ->rchild指針爲空,所以對齊進行線索化,也就是節點 F 的後繼節點爲節點 C (下圖中 粉色 表示)。

  然後對節點 C右子樹進行線索化可以看見此爲空,並且中序遍歷到此結束,所以最後一個節點也是沒有後繼節點,所以,節點 C 的有指針域指向 NULL (下圖中 藍色 表示)。
  
在這裏插入圖片描述

圖2.5 中序線索化的示意圖5

  
  感覺這個圖比較亂,但是由於後面這部分其實就是前面三張圖中的重複過程,所以弄明白那前面的三個過程,後面這個圖片只要對應好顏色,還是比較明顯能看出來的。

  所以,前面線索化完成後的示意圖可以表示爲下圖這樣。
  
在這裏插入圖片描述

圖2.6 中序線索化後的示意圖

  
  至此,中序線索化的過程已經說明完成了。

2.2、遍歷過程說明

  說起遍歷,那就是找到後繼節點,一個一個遍歷就是了,那麼中序遍歷也是,那麼應該如何才能在線索化後找到對應的後繼節點呢?
  在上圖2.6中,我們可以清楚地看到:

  1、節點 A 的後繼節點是節點 F ,但是節點 ArTag0右指針域指向的是其右子樹,那麼怎麼才能確定其後繼接單就是節點 F 呢,我們在看看節點 F ,爲節點 C左子樹,所以節點 F 跟節點 A 的關係就是:

    節點 A 的右子樹的最左下角的節點(最左的節點)

  2、節點 G 的後繼節點是節點 B ,而此時節點 GrTag1 ,是爲後繼節點的線索。包括節點 E 也是一致的。

2.3、線索化及遍歷的代碼實現

  綜上所述,中序線索化的算法過程可以描述爲:

  在中序遍歷的基礎上,將原本遍歷函數中輸出的語句修改成線索化的代碼即可。

  具體線索化的部分代碼爲:

    1、如果當前節點 current 左指針域爲空,則需要將其做指針域加上前驅線索
    2、如果當前節點的上一次訪問的節點( prev 指針)右子樹爲空,則需要爲 prev 指針指向後繼節點
    3、使 prev 指針跟上當前節點指針

  所以線索化代碼代碼可以這樣編寫。

/**
 *  功 能:
 *      線索化二叉樹 -- 中序線索化
 *  參 數:
 *      root:要線索化的樹的根節點
 *      ptmp:用於保留前驅節點
 *  返回值:
 *      無
 **/
void in_thread(BiTNode *root, BiTNode **ptmp)
{
    if ((root == NULL)) goto END;

    in_thread(root->lchild, ptmp);

    if (root->lchild == NULL)
    {
        root->lchild = *ptmp;
        root->lTag = 1;
    }
    if (*ptmp != NULL && (*ptmp)->rchild == NULL)
    {
        (*ptmp)->rchild = root;
        (*ptmp)->rTag = 1;
    }

    (*ptmp) = root;
    in_thread(root->rchild, ptmp);
END:
    return;
}

/**
 *  功 能:
 *      中序線索化二叉樹的後繼節點 
 *  參 數:
 *      root:要查找的節點
 *  返回值:
 *      成功:節點的後繼節點
 *      失敗:NULL
 **/
BiTNode *in_thread_nextNode(BiTNode *root)
{
    BiTNode *ret = NULL;

    if (root == NULL) goto END;

    if (root->rTag == 1) // 右標誌位 1,可以直接得到後繼節點
    {
        ret = root->rchild;
    }
    else // 右標誌位0,則要找到右子樹最左下角的節點
    {
        ret = root->rchild;
        while (ret->lTag == 0) // 查找最左下節點的位置
        {
            ret = ret->lchild;
        }
    }

END:
    return ret;
}

/**
 *  功 能:
 *      遍歷線索化二叉樹 - 中序正常順序
 *  參 數:
 *      root:要遍歷的線索二叉樹的根節點
 *  返回值:
 *      無
 **/
void in_thread_Older_next(BiTNode *root)
{
    if (root == NULL) goto END;

    while (root->lTag == 0) // 查找中序遍歷的第一個節點,當lTag == 0的話
        root = root->lchild;

    printf("%c ", root->data); // 訪問第一個節點

    while (root->rchild != NULL) // 當root存在後繼,依次訪問後繼節點
    {
        root = in_thread_nextNode(root);
        printf("%c ", root->data);
    }

END:
    printf("\n");
    return;
}

  至於上面代碼中爲什麼將 prev 指針作爲二級指針傳入是因爲遞歸過程衝需要改變 prev 指針指向的地址中的值,想要修改一級指針的話就需要傳入一級指針的地址,也就是二級指針。如果還是想不明白的,請移步看看指針的內容。

2.4、線索化的測試

2.4.1、測試案例工程結構

  爲了兼容 unixwindows 系統以及方便進行工程管理,特意使用 Cmake 工具進行編譯等,目前測試工程的目錄結構如下所示。

biTree-threaded-in/
├── CMakeLists.txt
├── README.md
├── image
│   └── image.jpg
├── main
│   └── main.c
├── runtime
└── src
    ├── biTree
    │   ├── biTree.c
    │   └── biTree.h
    └── thread
        ├── thread.c
        └── thread.h

6 directories, 8 files

2.4.2、測試案例代碼

  前面已經說明整體工程的結構,以及需要的文件,下面是測試底層功能函數的測試demo,詳細代碼如下。

#include "../src/thread/thread.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//ABDH#K###E##CFI###G#J##

int main(int argc, const char *argv[])
{
    int ret = 0;
    BiTNode *tree = NULL;

    printf("請按照先序輸入二叉樹(空節點用#表示):");
    tree = fBiTree.create();

    if (tree != NULL)
    {
        printf("\n二叉樹創建成功!\n");
    }
    else
    {
        printf("\n二叉樹創建出現異常!\n");
        ret = -1;
        goto ERROR_END;
    }

    printf("中序遍歷輸出:");
    fBiTree.inOrder(tree);
    printf("\n\n");

    int leaf = 0;
    fBiTree.leafNum(tree, &leaf);
    printf("節點的個數 = %d  葉子節點的個數 = %d, 樹的深度/高度 = %d\n\n", fBiTree.nodeNum(tree), leaf, fBiTree.depth(tree));

    BiTNode *copyTree = fBiTree.copy(tree);

    BiTNode *tmp = NULL;
    fthread.in(copyTree, &tmp);

    printf("\ncopy樹中序線索化遍歷輸出:");
    fthread.inNextOlder(copyTree);
    printf("\n");

    tmp = NULL;

ERROR_END:
    // 釋放二叉樹
    fBiTree.release(tree);
    /* copyTree已經在線索化的過程中被修改,此時按照原先的釋放方式釋放的話出現異常 */
    // fBiTree.release(copyTree);

    printf("system exit with return code %d\n", ret);

    return ret;
}

2.4.3、測試效果圖

  本次測試是在 windows 環境下進行,其他系統等詳細說明在 README.md 中查看。
  使用 Cmake 編譯,經過cmake編譯之後,配置cmake可執行文件放在固定目錄runtime下,可以使用在當前目錄下使用指令 ./../runtime/biTree.exe來運行可執行程序,也可以進入到目錄runtime中,然後使用指令 ./biTree.exe ,即可運行測試程序。

cd build
cmake -G"MinGW Makefiles" .. # 注意 .. ,表示上級目錄
make
./../runtime/biTree.exe

  實際測試的結果如下圖所示。

在這裏插入圖片描述

圖2.7 中序線索化後的示意圖

  

三、先序線索化及其遍歷

3.1、線索化過程說明

  開始先序線索化:在先序遍歷的基礎上進行線索化,
  首先在中序線索化之前,先定義兩個指針變量,用來輔助我們說明。

     current : 爲當前訪問的節點
     prev  : 爲之前訪問的最後一個節點,也就是 current 之前訪問的節點。

  對於先序遍歷,從根節點開始遍歷,所以此時,current 指針指向節點 Aprev 指針爲 NULL (下圖 黑色 表示)。

  1、在當前節點 A 下,其左子樹指針域不爲空,所以不需要進行線索化操作。並且此時 prev 指針指向 NULL ,所以也無需線索化。然後讓 prev 跟上 current 指針(下圖中用 粉色虛線 表示)。

  然後繼續遍歷,將 current 指針移動到節點 B 上,此時 B 節點同樣左子樹不爲空,也不需要進行線索化(下圖中用 粉色 表示)。並且此時 prev 指針指向節點 A ,其右子樹不爲空,所以也無需線索化。然後讓 prev 跟上 current 指針(下圖中用 紫紅色虛線 表示)。

  2、然後繼續遍歷,將 current 指針移動到節點 D 上,此時節點 D 左子樹爲空,所以需要進行線索化爲前驅,此時需要(下圖中用 紫紅色 表示,代碼連接部分用 紅色 表示)。

    current->lTag = 1;
    current->lchild = prev;

  此時 prev 指針指向節點 B ,其右子樹不爲空,所以也無需線索化。
  然後讓 prev 跟上 current 指針(下圖中用 紫色虛線 表示)。

在這裏插入圖片描述

圖3.1 先序線索化後的示意圖1

  
  3、然後繼續遍歷,按照之前遞歸遍歷來說,此時應該去遍歷訪問節點 D左子樹,但是因爲之前我們已經將節點 D左指針域線索化成前驅節點了,那麼在這個地方無法分辨,所以我們還需要告知遍歷程序,此時這個左指針域爲節點本身的左子樹還是前驅節點。另外在線索化的時候同時修改了 lTag 域,用 lTag 來分辨是左子樹還是前驅節點。所以,在線索化左子樹之前需要加一個判斷條件:
    if(lTag == 1)
  同樣地、對於右子樹同樣也需要判斷。
    if(rTag == 1)

  4、然後繼續遍歷,此時一判斷, current->lTag == 1 , 說明左指針域保存的是前驅節點,應該去判斷節點 D 的右子樹了,然後去判斷 current->rTag ,所以將 current 指針移動到節點 G 上,此時節點 G左子樹爲空,所以需要進行線索化爲前驅,此時需要(下圖中用 紫色 表示,代碼連接部分用 紅色 表示)。

    current->lTag = 1;
    current->lchild = prev;

  此時 prev 指針指向節點 D ,其右子樹不爲空,所以也無需線索化。

  然後讓 prev 跟上 current 指針(下圖中用 天藍色虛線 表示)。

在這裏插入圖片描述

圖3.2 先序線索化後的示意圖2

  
  5、然後繼續遍歷,由遞歸遍歷順序,此時 current 應該去遍歷節點 E ,但在遞歸返回的過程中到達節點 B 時候,判斷節點B的右標誌 current->rTag == 0 ,所以此時一判斷,說明右指針域保存的是右子樹,需要繼續便利線索化,因爲節點 E葉子節點,左右指針與均爲空,所以經過條件判斷,將節點 E左指針域線索化爲前驅(下圖中 天藍色 表示,線索化用 紅色 表示)。

  然後判斷 prev->rchild 不爲空,所以說明需要將 prev右指針域線索化爲後繼,所以就有(下圖中用 藍色 表示線索化)。

    prev->rchild = current;
    prev->rTag = 1;

  然後讓 prev 跟上 current 指針(下圖中用 草綠色虛線 表示)。

  6、繼續遍歷, current 指針移動到節點 C ,此時節點 C 存在左子樹,所以不予線索化。
  然後因爲此時 prev 指向節點 E ,其右指針域爲空,所以需要線索化爲後繼,所以應該線索化(下圖中 藍色 表示)。
  然後讓 prev 跟上 current 指針(下圖中用 橙色虛線 表示)。

  7、繼續遍歷, current 指針移動到節點 F ,此時節點 F 左指針域爲空,需要進行線索化(線索化用下圖 紅色 表示)。
  然後因爲此時 prev 指向節點 C ,其右指針域爲空,所以需要線索化爲後繼,所以應該線索化(線索化下圖中 藍色 表示)。
  然後讓 prev 跟上 current 指針(下圖中用 綠色虛線 表示)。

  8、節點 F 不存在左右子樹,所以遞歸遍歷返回,此時 current 指針移動到節點 C (下圖中 綠色 表示),此時節點 CrTag1右指針域爲後繼線索,所以不需要再次操作。再次直接返回,遍歷線索化過程結束。
  此時, F右指針域沒有進行操作,那麼按照原來的定義,節點 F 的右指針域還是指向爲 NULL 。所以整個過程可以用下圖表示。

在這裏插入圖片描述

圖3.3 先序線索化後的示意圖3

  
  可以看出來,上述圖中的線索化的表示與上面圖中1.2中的表示一致(點我可以查看圖1.2。。)。

3.2、遍歷過程說明

  遍歷,最主要的就是在知道的開頭的時候怎麼找到下一個該出現的節點。那麼對於先序線索化,想要找到某個節點的後繼節點還是非常容易的,主要可以:

    1、如果節點的 lTag0 ,那就說明是當前節點的左孩子,直接就是應該的後繼節點
    2、如果節點的 lTag 不爲 0 ,那麼其後繼節點就是其有孩子。

  所以、後序線索化的遍歷還是比較簡單的可以實現的。

3.3、線索化及遍歷的代碼實現

  綜上所述,那麼這個實現的代碼就可以這樣寫了。

/**
 *  功 能:
 *      線索化二叉樹 -- 先序線索化
 *  參 數:
 *      root:要線索化的樹的根節點
 *      ptmp:用於保留前驅節點
 *  返回值:
 *      無
 **/
void prev_thread(BiTNode *root, BiTNode **ptmp)
{
    if ((root == NULL)) goto END;

    if (root->lchild == NULL)
    {
        root->lchild = *ptmp;
        root->lTag = 1;
    }
    if (*ptmp != NULL && (*ptmp)->rchild == NULL)
    {
        (*ptmp)->rchild = root;
        (*ptmp)->rTag = 1;
    }

    (*ptmp) = root;

    // 如果不判斷就會造成死循環,因爲在程序一開始就對其左右孩子爲NULL時就做了前驅後繼的處理,同時改了標誌位,所以此處可根據標誌位判斷是子樹還是前驅後繼
    if (root->lTag == 0)
        prev_thread(root->lchild, ptmp);
    //同上
    if (root->rTag == 0)
        prev_thread(root->rchild, ptmp);
END:
    return;
}

/**
 *  功 能:
 *      遍歷線索化二叉樹 - 使用先序線索化的標識
 *  參 數:
 *      root:要遍歷的線索二叉樹的根節點
 *  返回值:
 *      無
 **/
void prev_thread_Older_normal(BiTNode *root)
{
    if (root == NULL) goto END;

    BiTNode *p = root;
    while (p != NULL)
    {
        while (p->lTag == 0)
        {
            printf("%c ", p->data);
            p = p->lchild;
        }
        printf("%c ", p->data);
        p = p->rchild;
    }

END:
    printf("\n");
    return;
}

3.4、線索化的測試

  在前面我們已經詳細的說明了測試工程的結構、Cmake相關編譯的指令等,此處也不再贅述。詳細結構可以 點我查看詳細結構與指令。

  這裏面也包含測試案例的代碼,只是將其中線索化和遍歷部分的代碼換成了先序線索化相關的代碼,此處也不再贅述。詳細結構可以 點我查看詳細代碼。

  詳細的先序線索化的測試效果如下圖所示。

在這裏插入圖片描述

圖3.4 先序線索化的測試效果

  

四、後序線索化及其遍歷

4.1、線索化過程說明

  我們已經非常清楚了,線索化是建立在遍歷的基礎上。那麼在後續遍歷中,首先遍歷訪問的是左子樹和右子樹,然後纔會訪問到根節點,所以在線索化的時候也就沒有先來後到的說法了,類似於退着走路就不需要管前面的路了。

  那麼根據線索化的特點,後續線索化的過程可以總結這樣:

  當前節點 current左指針域爲空 的時候,將 左指針域線索化爲其前驅
  當前節點 current右指針域爲空 的時候,將 右指針域線索化爲其後繼

  所以,後序線索化後的效果圖如下圖所示。
在這裏插入圖片描述

圖4.1 後序線索化後的效果圖

  

  在前面我們已經詳細的說明了測試工程的結構、Cmake相關編譯的指令等,此處也不再贅述。詳細結構可以 點我查看詳細結構與指令。

  這裏面也包含測試案例的代碼,只是將其中線索化和遍歷部分的代碼換成了先序線索化相關的代碼,此處也不再贅述。詳細結構可以 點我查看詳細代碼。

4.2、遍歷過程說明

  後續線索化,從上面的線索化過程中,以及本來的後序遍歷的過程中,我們已經知道,後序遍歷需要知道其雙親結點,所以我們在遍歷某個節點的時候,需要在某個地方來記錄一下其雙親節點,那麼也在無形中增加了遍歷的複雜度,所以在一定程度上也背離了我們線索化的初衷。並且在二叉鏈表的結構下,想要找到雙親節點很困難。這裏爲了演示測試效果我們採用一種方式採用後序來遍歷線索化後的二叉樹。其主要實現的思路就是:
    1、先按照 根節點->右孩子->左孩子 的方式來遍歷訪問節點,並且將順序記錄一下
    2、將剛纔記錄的順序翻轉即可

  詳細的思路可以查看博文:數據結構(十三) – C語言版 – 樹 - 二叉樹的遍歷(遞歸、非遞歸)。

4.3、線索化及遍歷的代碼實現

  綜上所述,那麼這個實現的代碼就可以這樣寫了。

/**
 *  功 能:
 *      線索化二叉樹 -- 後序線索化
 *  參 數:
 *      root:要線索化的樹的根節點
 *      ptmp:用於保留前驅節點
 *  返回值:
 *      無
 **/
void post_thread(BiTNode *root, BiTNode **ptmp)
{
    if ((root == NULL)) goto END;

    post_thread(root->lchild, ptmp);
    post_thread(root->rchild, ptmp);

    if (root->lchild == NULL)
    {
        root->lchild = *ptmp;
        root->lTag = 1;
    }
    if (*ptmp != NULL && (*ptmp)->rchild == NULL)
    {
        (*ptmp)->rchild = root;
        (*ptmp)->rTag = 1;
    }
    (*ptmp) = root;

END:
    return;
}

/**
 *  功 能:
 *      後序線索化二叉樹的前驅節點 
 *  參 數:
 *      root:要查找的節點
 *  返回值:
 *      成功:節點的後繼節點
 *      失敗:NULL
 **/
BiTNode *post_thread_prevNode(BiTNode *root)
{
    BiTNode *ret = NULL;

    if (root == NULL)
        goto END;

    // 如果 lTag 爲 1, 就是本應該的前驅節點
    if (root->lTag == 1)
        ret = root->lchild;
    // 如果右孩子存在並且 rTag 不爲 1, 那麼 rchild 指針域就是前驅節點
    else if (root->rchild && root->rTag != 1)
        ret = root->rchild;
    // 如果 rTag 爲 0, 並且同時 rTag 爲0, 那麼 rchild 指針域就是前驅節點
    // 這是因爲在左右子樹都存在的情況下,不會去進行線索化,但是其節點總歸要前驅
    // 節點和後繼節點的其中一個
    else if (root->lTag == 0 && root->rTag == 1)
        ret = root->lchild;
    else
        ret = root->lchild;

END:
    return ret;
}

/**
 *  功 能:
 *      遍歷線索化二叉樹 - 使用前驅節點
 *  參 數:
 *      root:要遍歷的線索二叉樹的根節點
 *  返回值:
 *      無
 **/
void post_thread_Older_byPrevNode(BiTNode *root)
{
    if (root == NULL)
        goto END;

    /**
     *  定義一個緩存區,用於保存 反向 後序遍歷的順序
     *  其中這個地方比較推薦使用的棧,省去多餘空間的浪費
     */
    unsigned char buf[128] = {0};
	int i = 0, j = 0;
    while (root)
    {
        buf[i] = root->data;
        root = post_thread_prevNode(root);
        i++;
    }

    for (j = i; j > 0; j--)
        printf("%c ", buf[j - 1]);

END:
    printf("\n");
    return;
}

4.4、線索化的測試

  在前面我們已經詳細的說明了測試工程的結構、Cmake相關編譯的指令等,此處也不再贅述。詳細結構可以 點我查看詳細結構與指令。

  這裏面也包含測試案例的代碼,只是將其中線索化和遍歷部分的代碼換成了後序線索化相關的代碼,此處也不再贅述。詳細結構可以 點我查看詳細代碼。

  詳細的先序線索化的測試效果如下圖所示。

在這裏插入圖片描述

圖4.2 後序線索化後的測試效果圖

  

五、總結

  根據每一種已經提到過的線索化的方式,進行一下簡單的總結,如下圖中表格所示。
  總結的內容爲一家之言,不一定準確也不一定全正確,如果您有不同意的觀點先請不要抨擊,歡迎交流,嘻嘻 ≧◠◡◠≦✌
  
在這裏插入圖片描述

圖5.1 線索化總結

  
  好啦,廢話不多說,總結寫作不易,如果你喜歡這篇文章或者對你有用,請動動你發財的小手手幫忙點個贊,當然關注一波那就更好了,好啦,就到這兒了,麼麼噠(*  ̄3)(ε ̄ *)。
在這裏插入圖片描述
上一篇:數據結構(十六) – C語言版 – 樹 - 二叉樹的線索化及遍歷 – 左指針域線索化、順序表線索化、鏈表線索化
下一篇:數據結構(十八) – C語言版 – 樹 - 二叉樹的線索化及遍歷 – 線索化後的直接前驅、後繼獲取

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