線索二叉樹 Threaded BinaryTree

weixin_34321977發表於2017-09-29

一、原理

利用二叉樹中閒置的 n+1 個空間記錄每個節點的前驅和後繼。

線索:利用二叉樹空鏈域存放在某種遍歷次序下結點的前驅和後繼,這些指標稱為線索,加上線索的二叉樹稱為線索二叉樹。根據線索性質的不同,線索二叉樹可分為前序、中序、後序三種。
線索化的過程就是在遍歷的過程中修改空指標的過程。

記 ptr 指向二叉連結串列中的一個結點,以下是建立中序線索的規則:

  • ptr->lchild 為空,則存放該結點的前驅結點,這個結點稱為 ptr 的中序前驅;
  • ptr->rchild 為空,則存放該結點的後繼結點,這個結點稱為 ptr 的中序後繼;

顯然,在決定 lchild 是指向左孩子還是前驅,rchild 是指向右孩子還是後繼,需要一個區分標誌的。因此,我們在每個結點再增設兩個標誌域 ltag 和 rtag,注意 ltag 和 rtag 只是區分0或1數字的布林型變數,其佔用記憶體空間要小於像 lchild 和 rchild 的指標變數。

結點結構重新定義如下:

  • ltag 為 0 時指向該結點的左孩子,為 1 時指向該結點的前驅;
  • rtag 為 0 時指向該結點的右孩子,為 1 時指向該結點的後繼;
1877813-81edee229a782563.png
普通二叉樹
1877813-455f6821b7897284.png
線索二叉樹 節點結構

二、原始碼

/**
 * 線索化即在遍歷過程中修改空指標
 * n 個結點的二叉樹一共有 2n 個指標,n-1 已經用於 link,剩下 n+1 個用於 thread
 * 剩下的:n-1 個用於按照遍歷的順序聯絡結點,多出的 2 個用於指向給出的頭結點
 */
#include <iostream>

using namespace std;

typedef enum {
    link, thread // 線索儲存標誌位:link(0),表示指向左右指標,thread(1),表示指向前趨後繼的線索
} tag;

typedef struct Bnode {
    char data;
    struct Bnode *Lchild, *Rchild;
    tag Ltag, Rtag; // 多了2個標誌位
} Bnode, *Bintree;

Bnode *pre; // 全域性變數,始終指向剛剛訪問過的結點

/**
 * 先序建立二叉樹 TLR
 * @param T 引用 main 函式中定義的樹根結點的地址
 */
void createBintree(Bintree &T) {
    char ch;
    cin >> ch;
    getchar(); // 接收 space 或 enter

    if (ch == '#') { // # 結束符
        T = NULL;
    } else {
        T = (Bintree) malloc(sizeof(Bnode)); // 新建結點
        T->data = ch;
        cout << ch << " 左結點(#結束): ";
        createBintree(T->Lchild);
        cout << ch << " 右結點(#結束): ";
        createBintree(T->Rchild);
    }
}

/**
 * 在中序遍歷基礎上,線索化聯絡結點,n-1
 */
void inThread(Bintree T) {
    if (T) {
        inThread(T->Lchild); // 左子樹線索化

        // 原本中序遍歷輸出資料的地方 變為修改空指標
        // 線索化結點的兩種方式:前趨,後繼
        if (!T->Lchild) {
            T->Ltag = thread;   // 前趨線索
            T->Lchild = pre;    // 當前結點T 找前趨pre
        }
        if (!pre->Rchild) {
            pre->Rtag = thread; // 後繼線索
            pre->Rchild = T;    // pre結點找後繼T (T在遞迴遍歷過程中可以是樹上任意一個結點)
        }
        pre = T; // 始終指向剛剛訪問過的結點 即下一輪當前結點的上一個結點

        inThread(T->Rchild); //遞迴右孩子線索化
    }
}

/**
 * 完整化線索二叉樹 新增頭結點 設定結束結點指向
 * @param head 線索二叉樹的頭結點
 * @param T 原本的二叉樹
 */
void inThreadTree(Bnode *head, Bintree T) {
    if (!T) {
        // 若二叉樹為空,則將Lchild和Rchild指向自己(雙向連結串列head前趨和後繼指向自己,連結串列為空)
        head->Lchild = head;
        head->Ltag = link;
        head->Rchild = head;
        head->Rtag = link;
    } else {
        // 起始工作
        head->Lchild = T;       // head左指標記錄下樹根,這樣在inThreadTraverse方法中只需要傳入head就夠了
        head->Ltag = link;
        head->Rchild = head;    // head右指標一定會線索化,先賦初值指向自己(head)
        head->Rtag = thread;
        pre = head;             // pre 賦初值,指向head,然後中序線索化

        // 中序線索化
        inThread(T);

        // 收尾工作
        pre->Rtag = thread;     // 線索化之後,pre是遍歷的最後一個結點
        pre->Rchild = head;
        head->Rchild = pre;     // 在起始工作中,先給了head->Rchild一個初值,這裡賦給了真正的值
    }
}

/**
 * 中序遍歷線索二叉樹
 * @param head 線索二叉樹的頭結點
 */
void inThreadTraverse(Bintree head) {
    Bnode *p = head->Lchild;    // 起始工作中設定了head->Lchild為樹根

    // 1.滿足樹為空的情況,樹空時設定了head左右孩子都指向自己
    // 2.滿足迴圈終結條件,收尾工作中設定了最後一個節點的Rchild為head
    while (p != head) {

        // 尋找線索開頭 從根尋找到最左結點
        while (p->Ltag == link) {
            p = p->Lchild;
        }
        cout << p->data << " ";

        // 根據線索,用連結串列的方式遍歷
        while (p->Rtag == thread && p->Rchild != head) {
            p = p->Rchild;
            cout << p->data << " ";
        } // 該迴圈一般在 p->Rtag == link 時跳出,說明右邊是一棵樹,需要重新尋找右子樹的最左節點

        p = p->Rchild;
    }
}

/**
 * 普通中序遍歷
 */
void inOrder(Bintree T) {
    if (T) {
        inOrder(T->Lchild);
        cout << T->data << " ";
        inOrder(T->Rchild);
    }
}

int main() {

    // 建立樹
    Bintree bintree;
    cout << "輸入樹的根結點: ";
    createBintree(bintree);
    cout << endl;

    // 普通中序遍歷
    cout << "普通中序遍歷" << endl;
    inOrder(bintree);
    cout << endl << endl;

    // 線索二叉樹並遍歷
    Bnode *head = (Bnode *) malloc(sizeof(Bnode)); // 頭結點
    inThreadTree(head, bintree);
    cout << "線索化後中序遍歷" << endl;
    inThreadTraverse(head);
    cout << endl << endl;

    return 0;
}
1877813-d57357465ad3e828.png
輸入樹的根結點: 1
1 左結點(#結束): 2
2 左結點(#結束): #
2 右結點(#結束): #
1 右結點(#結束): 3
3 左結點(#結束): 4
4 左結點(#結束): #
4 右結點(#結束): #
3 右結點(#結束): 5
5 左結點(#結束): #
5 右結點(#結束): #

普通中序遍歷
2 1 4 3 5 

線索化後中序遍歷
2 1 4 3 5 

誤區

本來想用做個嘗試,線上索化二叉樹後,隨便取一個結點,輸出其前趨和後繼。

原本採用先序遍歷的方式尋找:

void preOrderSearch(Bintree T, char target) {
    if (T) {
        if (T->data == target) {
            if (T->Ltag == thread) cout << "前趨結點:" << T->Lchild->data << endl;
            if (T->Rtag == thread) cout << "後繼結點:" << T->Rchild->data << endl;
            return;
        }
        preOrderSearch(T->Lchild, target);
        preOrderSearch(T->Rchild, target);
    }
}

錯誤點:if (T) 的條件控制已經不對了,線索化之後,不存在空結點了。

相關文章