數據結構(十八) -- C語言版 -- 樹 - 二叉樹的線索化及遍歷 -- 線索化後的直接前驅、後繼獲取

零、讀前說明

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

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

    數據結構(十六) – C語言版 – 樹 - 二叉樹的線索化及遍歷 – 左指針域線索化、順序表線索化、鏈表線索化

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

  上面這個系列已經詳細的說明了線索化的方式、代碼、遍歷等。但是既然提到了線索化,那麼還是會很好奇線索化後某個節點的前驅節點、後繼節點應該怎麼取獲取。那麼下面就開始說明方法並貼出代碼。

一、先序線索化的前驅和後繼

  首先來一個先序線索化的效果圖,如下圖所示。
  
在這裏插入圖片描述

圖1.1 先序線索化的效果示意圖

  

1.1、前驅節點

  右上圖中可以看出來,想要找到某個節點的前驅節點很困難。其中比如:

  節點 C 的前驅節點爲節點 F ,但是在當前這種二叉鏈表結構中,我們只有兩個指針域( lchildrchild ),所以我們想單獨通過某個節點去查找其前驅節點的話無法確定(無法確定其雙親節點)。

  想要實現獲取前驅節點,那麼可以有兩種方式去實現。

  第一種:既然需要獲取前驅節點,那麼在不修改節點的結構的情況下(二叉鏈表),可以線索成順序表、雙向鏈表等,那麼即可完美滿足。那麼詳細情況可以參考博文:

  數據結構(十六) – C語言版 – 樹 - 二叉樹的線索化及遍歷 – 左指針域線索化、順序表線索化、鏈表線索化

  在二叉鏈表結構下, 也可以通過一定的方式找到其雙其節點,但是確定就是必須要傳入樹的根節點以及要求雙親節點的節點。所以,在二叉鏈表的結構下,即使知道其雙親節點也不能完全獲取其前驅節點,因爲在對某一個節點求取前驅節點的時候,並不知道其所在二叉樹的根節點。那麼,碎玉某一個節點的雙親節點,可以通過下面的代碼獲取。

/**
 *  功 能:
 *      先序線索化二叉樹的雙親節點
 *  參 數:
 *      root :樹的根節點
 *      child:要獲取雙親節點的節點
 *  返回值:
 *      成功:child節點雙親節點
 *          注意:當child爲根節點的時候,返回也是NULL
 *      失敗:NULL
 **/
BiTNode *prev_thread_parent(BiTNode *root, BiTNode *child) //找孩子的雙親
{
    BiTNode *ret = NULL;

    if (root == NULL || child == NULL || root == child)
        goto END;

    //這裏已經將葉子節點指針域線索化成了前驅和後繼,所以得加另外的限制
    if ((root->lTag != 1 && root->lchild == child) || (root->rTag != 1 && root->rchild == child))
    {
        ret = root;
        goto END;
    }

    if (root->lTag != 1) //這裏同樣是要判斷是左右孩子還是前驅後繼,否則就會造成循環
        ret = prev_thread_parent(root->lchild, child);
    if (ret == NULL && root->rTag != 1)
        ret = prev_thread_parent(root->rchild, child);
END:
    return ret;
}

  第二種:既然需要確定雙親節點,那麼就修改節點的結構,使其節點結構中包含其雙親節點。那麼可以將二叉鏈表轉換成三叉鏈表。三叉鏈表節點結構定義。

typedef struct BiTNode
{
    TElemType data;         /* 節點數據 */
    struct BiTNode *lchild; /* 左孩子 */
    struct BiTNode *rchild; /* 右孩子 */
    struct BiTNode *parent; /* 雙親節點 */
    int lTag;
    int rTag;
} BiTNode;

1.1.1、三叉鏈表下二叉樹的創建與線索化

  在用三叉鏈表表示的輸的結構下, 創建一個二叉樹需要在創建節點的時候將其雙親節點通過參數的形式傳入,其創建方式與二叉鏈表的創建唯二的不同就是:

    1、傳入參數 — 雙親節點
    2、將雙親節點賦值與 parent 指針域

  綜上所述,代碼可以這樣寫了。

/**
 * 功 能:
 *      創建並且初始二叉樹 - 按照先序遍歷建立二叉樹
 * 參 數:
 *      parent : 當前節點的雙親節點
 * 返回值:
 *      成功:創建完成的樹的根節點
 *      失敗:NULL
 **/
BiTNode *BiTree_Create(BiTNode *parent)
{
    BiTNode *root = NULL;
    char ch;

    scanf("%c", &ch);

    // 如果字符值不爲 # ,則說明節點存在
    if (ch != '#')
    {
        // 爲節點申請空間
        root = (BiTNode *)malloc(sizeof(BiTNode));
        if (root == NULL) return NULL;

        memset(root, 0, sizeof(BiTNode));

        root->parent = parent;
        // 將字符值賦值給節點
        root->data = ch;
        // 遞歸創建左孩子,並將返回值賦值給左孩子
        root->lchild = BiTree_Create(root);
        // 遞歸創建右孩子,並將返回值賦值給右孩子
        root->rchild = BiTree_Create(root);
    }

    return root;
}

  對於二叉樹的線索化,不管是二叉鏈表還是三叉鏈表結構形式,都不會有任何改變,因爲線索化的過程不涉及到 parent 指針域。詳細線索化的的情況可以參考博文。

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

1.1.2、三叉鏈表下前驅節點

  上面弄了這麼多,不就是爲了一個簡簡單單的前驅節點麼。那麼根據線序線索化的過程,其前驅節點的特徵可以總結爲下面這樣:

  1、如果爲根節點,那麼前驅節點爲空
  2、如果節點的 lTag == 1 ,那麼前驅節點爲其左指針域所指
  3、如果節點的 lTag == 0 ,那麼:
    1)如果當前節點爲其雙親節點的左孩子,那麼其前驅節點爲其雙親節點
    2)如果當前節點爲其雙親節點的右孩子,那麼其前驅節點爲雙親節點的左子樹的第一個右孩子(參考上圖中1.1中節點 C 的前驅節點 E點我可以查看圖1.1。。)。

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

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

    if (node == NULL) goto END;

    if (node->parent == NULL) goto END;

    if (current->lTag == 1) // lTag 爲 1,是爲官方前驅節點
    {
        ret = current->lchild;
    }
    else
    {
        BiTNode *pCur = current; // 當前直接的臨時保存指針

        current = current->parent;
        if (pCur == current->lchild) // 是雙親節點的左孩子
        {
            ret = current;
            goto END;
        }
        else if (pCur == current->rchild) // 是雙親節點的右孩子
        {
            current = current->lchild;
        }

        while (current->rchild == NULL)
        {
            if (current->rTag == 0) // 如果rTag = 0,是當前節點的左子樹,不然就是後繼節點
                current = current->lchild;
        }
        // 當前節點的右孩子
        ret = current->rchild;
    }

END:
    return ret;
}

1.2、後繼節點

  在先序線索二叉樹中查找結點的後繼很容易。
  按照 先序遍歷的順序根節點 -> 左孩子 -> 右孩子)來說,其後繼節點:

    如果 lTag == 0 ,說明左指針域爲節點的左子樹,也就是後繼節點
    如果 rTag == 1 ,說明右指針域爲節點的後繼節點
    如果 rTag == 0 ,說明右指針域爲節點的右子樹,由於在前面已經判斷了左指針域的情況,說明左子樹已經訪問完畢,所以,右子樹即爲其後繼節點

  所以,代碼就是這麼的簡單了。

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

    if (node == NULL) goto END;

    if (node->lTag == 0) // 左標誌位 0,是爲其左孩子,也就是後繼節點
    {
        ret = node->lchild;
    }
    else // 如果rTag爲1,是爲正確後繼,如果rTag爲0,爲右孩子
    {
        ret = node->rchild;
    }

END:
    return ret;
}

  綜上所述,在原本二叉鏈表的形勢下,利用 lTagrTag 的標示的形式下的先序線索化,無法在不借助外部力量的情況實現前驅節點的獲取,所以說先序線索二叉樹是一種不完善的線索化

二、中序線索化的前驅和後繼

  首先來一箇中序線索化的效果圖,如下圖所示。
  
在這裏插入圖片描述

圖2.1 中序線索化的效果示意圖

  
  詳細線索化的的情況可以參考博文。

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

2.1、前驅節點

  在中序線索二叉樹中查找節點的前驅節點和後繼節點都很容易。就像傳說中的鄰家乖乖女一樣。。。。。

  按照 中序遍歷的順序左孩子 -> 根節點 -> 右孩子)來說,某個節點的前驅節點的求取過程可以總結爲這樣:

  1、如果當前節點的 lTag == 1 , 那麼其左指針域所指節點即爲前驅節點;
  2、如果當前節點的 lTag == 0 ,那麼:
    1)其左子樹不存在右孩子,那麼其左子樹爲其前驅節點
    2)其左子樹存在右孩子,那麼一直找到其最右邊的葉子節點,即爲前驅節點;

  所以,代碼就是這麼的簡單了。

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

    if (root == NULL) goto END;

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

END:
    return ret;
}

2.2、後繼節點

  按照 中序遍歷的順序左孩子 -> 根節點 -> 右孩子)來說,某個節點的後繼節點的求取過程可以總結爲這樣:

  1、如果當前節點的 rTag == 1 ,那麼其右指針域指向即爲其後繼節點
  2、如果當前節點的 rTag == 0 ,那麼該結點右子樹最左邊的尾結點就是它的線性後繼結點
    1)其右子樹不存在左孩子,那麼其右子樹爲其後繼節點
    2)其右子樹存在左孩子,那麼一直找到其最左邊的葉子節點,即爲後繼節點(其實正好和前驅節點的相反);

  所以,代碼就是這麼的簡單了。

/**
 *  功 能:
 *      中序線索化二叉樹的後繼節點 
 *  參 數:
 *      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;
}

  綜上所述,在原本二叉鏈表的結構下,利用 lTagrTag 的標誌的中序線索化,既可以非常方便簡單的獲取到前驅節點、也可以很容易的得到其後繼節點, 所以中序線索二叉樹是一種完善的線索化,因此在線索化的領域出現的最爲頻繁。

三、後序線索化的前驅和後繼

  首先來一個後序線索化的效果圖,如下圖所示。
  
在這裏插入圖片描述

圖3.1 後序線索化的效果示意圖

3.1、前驅節點

  按照 後序遍歷的順序左孩子 -> 右孩子 -> 根節點)來說,想要獲取某個節點的前驅節點,那麼也就是獲取根節點的右孩子、某個節點的右孩子的某個節點的左孩子…相對而言,前驅節點的獲取比較簡單…所以某個節點的前驅節點的求取過程可以總結爲這樣:

  1、如果節點的 lTag = 1,那麼 lchild 指針域所指即爲其前驅節點
  2、如果節點存在右孩子並且 rTag 不爲 1 ,那麼 lchild 指針域就是其前驅節點
  3、如果節點的 rTag = 0 , 並且同時 rTag = 0 , 那麼 lchild 指針域所指就是其前驅節點
  4、如果上面的條件都不滿足,那麼 lchild 指針域所指即爲其前驅節點

  所以,代碼就可以這麼編寫了。

/**
 *  功 能:
 *      後序線索化二叉樹的前驅節點 
 *  參 數:
 *      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;
}

3.2、後繼節點

  前文中我們已經對於後序線索化的後繼節點的獲取有過簡單的描述,並且使用投機取巧的方式完成了在二叉鏈表的結構下的後序線索化二叉樹的遍歷。詳細的情況請參考博文:

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

  在不借助外部勢力的情況下,我們通過改造自身來實現後繼節點的獲取後續節點,那麼我們就需要將原本的二叉鏈表修改成三叉鏈表的結構。具體三叉鏈表相關內容請點擊下列鏈接查看。

   點我查看三叉鏈表結構定義

  點我查看三叉鏈表下二叉樹的創建與線索化

   點我查看獲取父節點的代碼

綜上所以資料的整合所述,那麼後序線索二叉樹中查找當前節點的後繼節點可以描述爲這樣:

  1、如果當前節點爲根節點,則無後繼節點
  2、如果當前節點爲其雙親的右孩子,則其後繼節點爲其雙親節點
  3、如果當前節點爲其雙親的左孩子,那麼:
    1)如果當前節點的雙親節點不存在右孩子,則其雙親節點爲其後繼節點
    2)如果當前節點的雙親節點存在右孩子,則其雙親節點的右子樹中按後序遍歷的第一個節點爲其後繼節點。

  所以,代碼就可以這麼編寫了。

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

    if (root->rTag == 1) // 官方指定的後繼節點
    {
        ret = root->rchild;
    }
    else
    {
        BiTNode *parent = root->parent; //prev_thread_parent(root);

        if (parent == NULL) // 根節點,無後繼節點
            ret = NULL;
        else if (root == parent->rchild) // 雙親的右孩子,則其後繼爲其雙親;
        {
            ret = parent;
        }
        else if (root == parent->lchild && parent->rTag == 1) // 雙親無右子女,則其後繼爲其雙親;
        {
            ret = parent;
        }
        else if (root == parent->lchild && parent->rTag == 0) //  雙親有右子女
        {
            root = parent->rchild;                  // 其雙親的右子樹中 按後序遍歷的第一個結點。
            while (root != NULL && root->lTag == 0) // 要求是左子樹
            {
                root = root->lchild;
            }

            if (root != NULL && root->rTag == 0) // 說明最左節點還有右孩子
            {
                root = root->rchild;
                if (root->lTag == 0) // 說明存在左孩子,需要移動到當前左孩子身邊
                {
                    while (root != NULL && root->lTag == 0)
                        root = root->lchild;
                }
            }

            ret = root;
        }
    }

END:
    return ret;
}

3.3、三叉鏈表下的後序線索化的遍歷

  那麼既然前面已經說明了三叉鏈表結構下的後繼節點的獲取的操作,那麼同樣的也可以將其遍歷弄出來,整體來說呢,線索化後的遍歷的過程其實就是一個獲取後繼節點的過程,所以,在前面後繼節點的獲取的基礎上,我麼可以將便利的代碼這樣寫。

/**
 *  功 能:
 *      遍歷線索化二叉樹 -- 常規遍歷
 *  參 數:
 *      root:要遍歷的線索二叉樹的根節點
 *  返回值:
 *      無
 **/
void post_thread_Older_normal(BiTNode *root)
{
    BiTNode *prev = NULL;

    if (root == NULL) goto END;

    while (root != NULL)
    {
        // 定位到樹最左邊的節點
        while (root->lchild != prev && root->lTag == 0)
            root = root->lchild;

        // root->rTag 訪問當前節點的後繼節點
        while (root != NULL && root->rTag == 1)
        {
            printf("%c ", root->data);
            prev = root;
            root = root->rchild;
        }

        // 如果上一次訪問記錄的節點與當前節點的右孩子重複,則說明當前節點的左子樹已經訪問完成
        while (root != NULL && root->rchild == prev)
        {
            printf("%c ", root->data);
            prev = root;
            root = root->parent;
        }

        // 開始遍歷右子樹
        if (root != NULL && root->rTag == 0)
        {
            root = root->rchild;
            if (root->lTag == 0) // 說明存在左孩子,需要移動到當前左孩子身邊
            {
                while (root != NULL && root->lTag == 0)
                    root = root->lchild;
            }
        }
    }

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

  當然,求後序線索二叉樹中結點的後繼要知道其雙親的信息。那麼:

    1、在三叉鏈表的結構形式下,我們直接使用其 parent 指針域來獲取其雙親節點
    2、在二叉鏈表的結構形式下,我們可以通過 的特性來保存當前節點的雙親節點

  綜上所述,在原本二叉鏈表的結構下,利用 lTagrTag 的標誌的後序線索化,獲取前驅節點比較方便容易,但是想要獲取後繼節點非常困難,需要知道其雙親節點才能獲取到前驅節點,所以需要三叉鏈表才能完成後繼節點的獲取。另外,在後續線索化的遍歷中,同樣需要獲取雙親節點,那麼在二叉鏈表的結構下遍歷需要藉助棧來實現雙親節點的保存。所以後序線索二叉樹也是一種不完善的線索化

四、總結

  關於各種線索化比較詳細的總結,可以參考博文:

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

  下面就是簡單總結關於各種方式線索化後的前驅節點、後繼節點的特性來說。

  1、先序線索二叉樹是一種不完善的線索化。

    前驅節點:只能使用三叉鏈表結構,二叉鏈表結構下的雙親節點也無法獲取
    後繼節點:二叉鏈表結構下可相對容易獲取

  2、中序線索二叉樹是一種完善的線索化。速度較一般二叉樹的遍歷速度快,且節約存儲空間。

    前驅節點:二叉鏈表結構下即可方便獲取,且任意一個節點都能直接找到它的前驅
    後繼節點:二叉鏈表結構下即可方便獲取,且任意一個節點都能直接找到它的後繼

  3、後序線索二叉樹也是一種不完善的線索化。

    前驅節點:二叉鏈表結構下可方便獲取
    後繼節點:二叉鏈表結構下需要藉助 模型,且其雙親節點也無法獲取。使用三叉鏈表結構可以獲取
  
  好啦,廢話不多說,總結寫作不易,如果你喜歡這篇文章或者對你有用,請動動你發財的小手手幫忙點個贊,當然關注一波那就更好了,好啦,就到這兒了,麼麼噠(*  ̄3)(ε ̄ *)。
在這裏插入圖片描述
上一篇:數據結構(十七) – C語言版 – 樹 - 二叉樹的線索化及遍歷 – 先序線索化、中序線索化、後序線索化
下一篇:數據結構(十九) – C語言版 – 樹 - 樹、森林、二叉樹的江湖愛恨情仇、相互轉換

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