【資料結構&演算法】12-線索二叉樹

李柱明發表於2021-11-12


前言

在《大話資料結構》P190 頁中有一句話:其實線索二叉樹,就等於是把一棵二叉樹轉變成了一個雙向連結串列。

對於這句話實在想不懂,線索二叉樹只是把二叉樹以某種次序遍歷把空域填上前驅或後繼而已,若度為 2 的結點沒有多餘的指標域用於線索了,那雙向連結串列就斷了啊。

李柱明部落格:https://www.cnblogs.com/lizhuming/p/15487406.html

線索二叉樹的概念

概念:

  • 引入原因:二叉連結串列中,只能看出左右孩子,而看不出某序遍歷的前驅和後繼。

  • 線索:指向前驅和後繼的指標稱為線索。

  • 線索連結串列:加上線索的二叉連結串列稱為線索連結串列。

  • 線索二叉樹(Threaded Binary Tree):加上線索的二叉樹稱為線索二叉樹。

    • 左域為空則改為前驅。
    • 右域為空則改為後繼。
    • 每個節點新增左右 tag,用於標記左右域指標是孩子還是線索。
  • 線索化:對二叉樹以某種次序遍歷使其變為線索二叉樹的過程。

    • 前、中、後序線索二叉樹。

線索二叉樹的實現

前驅和後繼的資訊只有在遍歷二叉樹時才能得到。而在遍歷二叉樹時便可以線索化。

線索化:

  • 在遍歷過程中修改空指標。

  • 空左域為前驅。

  • 空右域為後繼。

  • 實際實現:

    • 前、中、後序:正常的中序遍歷演算法。在遍歷過程中,把輸出資料部分改為線索化內容,即是修改空指標。

線索二叉樹的遍歷:

  • 中序:採用非遞迴方式:

    • 中序遍歷演算法思路:先找到最左下,再中,再切到右子樹找到右子樹的最左下,再中,再右...。

      • 簡單來說就是:左、中、右。先找最左下。切到右時也要先找右的最左下。
    • 實現步驟:

      1. 找最左下:一直迴圈找左孩子,直到找到 ltag 為線索的節點。

      2. 找到最左下節點後,輸出資料。

      3. 判斷該節點是否有後繼。

        1. 有:則直接跳到後繼節點。輸出資料。判斷該節點是否有後繼(就是重複步驟 3)。(線索)
        2. 無:則切到右孩子,對該右子樹做前序遍歷。即是找這個右孩子的最左下(重複步驟 1 、2、3)。(孩子)
        3. 提醒:左右域只有兩種情況:1. 線索;2. 孩子。

線索二叉樹節點:

/* 線索二叉樹節點結構 */
typedef int tree_elem_type;

/* link_e==0表示指向左右孩子指標, */
/* thread_e==1表示指向前驅或後繼的線索 */
typedef enum {link_e, thread_e} pointer_tag_e;

typedef struct tree_node
{
    struct tree_node *lchild; // 左子域
    struct tree_node *rchild; // 右兄域
    pointer_tag_e ltag; // 左標誌
    pointer_tag_e rtag; // 右標誌
    tree_elem_type data; // 資料
}binary_tree_node_t;

線索化程式碼參考下面參考程式碼。

線索二叉樹的尋點思路二

以中序線索二叉樹為例,令 P 所指節點的某個結點。查詢 p 所指結點的後繼點的方法:

  1. 若 p->rtag==1,則 p->rchild 指向其後繼節。

  2. 若 p->rtag==0,則 p 所指結點的中序後繼必然是其右子樹中進行中序遍歷時得到的第一個結點。也就是 p 的右子樹中“最左下”的結點。

    1. 這點可以利用遞迴。
    2. 也可以參考下面程式碼的非遞迴方法。

尋點程式碼參考下面程式碼。

類雙向連結串列參考圖

參考程式碼

中序遍歷線索化

  • 注意:in_threading(); 函式使用前需要對 pre 節點進行賦有效節點值。
  • 下面為類雙向連結串列線索化和線索遍歷參考程式碼
binary_tree_node_t *pre = NULL; /* 全域性變數, 上一訪問節點 */

/* 中序遍歷進行中序線索化 */
void in_threading(binary_tree_node_t *p)
{ 
    if(p)
    {
        in_threading(p->lchild); /* 遞迴左子樹線索化 */
        if(!p->lchild) /* 沒有左孩子 */
        {
            p->ltag = thread_e; /* 前驅線索 */
            p->lchild = pre; /* 左孩子指標指向前驅 */
        }
        if(!pre->rchild) /* 前驅沒有右孩子 */
        {
            pre->rtag = thread_e; /* 後繼線索 */
            pre->rchild = p; /* 前驅右孩子指標指向後繼(當前結點p) */
        }
        pre = p; /* 保持pre指向p的前驅 */
        in_threading(p->rchild); /* 遞迴右子樹線索化 */
    }
}

/* 中序遍歷二叉樹tree,並將其中序線索化,tree_list指向頭結點 */
int in_order_threading(binary_tree_node_t **tree_list_p, binary_tree_node_t *tree)
{ 
    *tree_list_p = (binary_tree_node_t *)malloc(sizeof(binary_tree_node_t));
    if(!*tree_list_p)
        return -1;

    (*tree_list_p)->ltag = link_e; /* 孩子,用於指根 */
    (*tree_list_p)->rtag = thread_e; /* 線索,只最右下。即是鏈尾 */
    (*tree_list_p)->rchild = (*tree_list_p); /* 初始化:右指標回指 */

    if(!tree) /* 若二叉樹空,則左指標回指 */
        (*tree_list_p)->lchild = *tree_list_p;
    else
    {
        (*tree_list_p)->lchild = tree;

        pre = (*tree_list_p);

        in_threading(tree); /* 中序遍歷進行中序線索化 */

        pre->rchild = *tree_list_p;

        pre->rtag = thread_e; /* 最後一個結點線索化 */

        (*tree_list_p)->rchild = pre;
    }

    return 0;
}

/* 中序遍歷二叉線索樹tree(頭結點)的非遞迴演算法 */
int in_order_traverse_tree_list(binary_tree_node_t *tree)
{ 
    binary_tree_node_t *p = NULL;

    p = tree->lchild; /* p指向根結點 */

    while(p != tree)
    { 
        /* 找到最左下節點 */
        while(p->ltag == link_e)
            p = p->lchild;

        if(!visit(p->data)) /* 訪問其左子樹為空的結點 */
            return -1;
  
        /* 判斷是否有後繼,且不是整個樹的最右下節點 */
        while(p->rtag == thread_e && p->rchild != tree)
        {
            p = p->rchild;
            visit(p->data); /* 訪問後繼結點 */
        }
        p = p->rchild; /* 切換到右子樹。然後繼續中序遍歷該右子樹 */
    }

    return -1;
}

相關文章