我們知道滿二叉樹只是一種特殊的二叉樹,大部分二叉樹的結點都是不完全存在左右孩子的,即很多指標域沒有被充分地利用。另一方面我們在對一棵二叉樹做某種次序遍曆的時候,得到一串字元序列,遍曆過後,我們可以知道結點之間的前驅後繼關係,也就是說,我們可以很清楚地知道任意一個結點,它的前驅和後繼是哪一個。可是這是建立在已經遍曆過的基礎之上的。在二叉鏈表上,我們只能知道每個結點指向其左右孩子結點的地址,而不知道某個結點的前驅是誰,後繼是誰。要想知道,必須遍曆一次。以後每次需要知道時,都必須遍曆一次。為什麼不考慮在建立時就記住這些前驅和後繼呢?那將是多大的時間上的節省。
綜合剛才兩個角度的分析後,我們可以考慮利用那些空地址,存放指向結點在某次遍曆次序下的前驅和後繼結點的地址。我們把這種指向前驅和後繼的指標稱為線索,加上線索的二叉鏈表稱為線索鏈表,相應的二叉樹就稱為線索二叉樹(Threaded Binary Tree)。
6-10-2,我們把這棵二叉樹進行中序遍曆後,將所有的null 指標域中的rchild,改為指向它的後繼結點。
6-10-3,我們把這棵二叉樹進行中序遍曆後,將所有的null 指標域中的lchild,改為指向它的前驅結點。
通過圖6-10-4(空心箭頭實線為前驅,虛線黑箭頭為後繼),更容易看出,其實線索二叉樹,等於是把一棵二叉樹轉變成了一個雙向鏈表,這樣對我們的插入刪除結點,尋找某個結點都帶來了方便。所以我們把對二叉樹以某種次序遍曆使其變為線索二叉樹的過程稱做是線索化。
為了區分指標域是指向左右孩子還是前驅後繼,需要再增加兩個標誌位ltag和rtag,ltag為0時表示指向左孩子,為1時表示指向前驅,rtag為0時表示指向右孩子,為1時表示指向後繼。和雙向鏈表結構一樣,可以在二叉樹線索鏈表上添加一個頭結點,6-10-6所示,這樣做的好處是我們既可以從第一個結點H開始順後繼進行遍曆(利用1,4兩根線),也可以從最後一個結點G開始順前驅進行遍曆(利用2,3兩根線),將頭結點作為遍曆結束的判據。
樣本程式如下:(改編自《大話資料結構》)
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
|
|
#include<iostream> using namespace std;#define MAXSIZE 50 typedef char ElemType; typedef enum { Link, Thread } PointerTag; typedef char String[MAXSIZE + 1]; //以'\0’結尾 String str; /* 用於構造二叉樹*/ /* 結點結構 */ typedef struct BThrNode { ElemType data;/* 結點資料 */ struct BThrNode *LChild;/* 左右孩子指標 */ struct BThrNode *RChild; PointerTag LTag; PointerTag RTag; } BThrNode, *BThrNodePtr; /* 構造一個字串 */ bool StrAssign(String Dest, char *ptr) { cout << "Assign Str ..." << endl; int i; for (i = 0; ptr[i] != '\0' && i < MAXSIZE; i++) Dest[i] = ptr[i]; Dest[i] = '\0'; return true; } bool CreateBThrTree(BThrNodePtr *Tpp) { ElemType ch; static int i = 0; if (str[i] != '\0') ch = str[i++]; if (ch == '#') *Tpp = NULL; else { *Tpp = (BThrNodePtr)malloc(sizeof(BThrNode)); if (!*Tpp) exit(1); (*Tpp)->data = ch;/* 產生根結點 */ CreateBThrTree(&(*Tpp)->LChild);/* 構造左子樹 */ if ((*Tpp)->LChild) (*Tpp)->LTag = Link; CreateBThrTree(&(*Tpp)->RChild);/* 構造右子樹 */ if ((*Tpp)->RChild) (*Tpp)->RTag = Link; } return true; } BThrNodePtr prev;/* 全域變數,始終指向剛剛訪問過的結點 */ /* 中序遍曆進行中序線索化 */ void InThreading(BThrNodePtr Tp) { if (Tp) { InThreading(Tp->LChild);/* 在第一次左遞迴過程中綁定了的線條3 */ if (!Tp->LChild)/* 沒有左孩子 */ { Tp->LTag = Thread;/* 前驅線索 */ Tp->LChild = prev;/* 左孩子指標指向前驅 */ } if (!prev->RChild)/* 前驅沒有右孩子 */ { prev->RTag = Thread;/* 後繼線索 */ prev->RChild = Tp;/* 前驅右孩子指標指向後繼(當前結點Tp) */ } prev = Tp; InThreading(Tp->RChild);/* 遞迴右子樹線索化 */ } } /* 中序遍曆二叉樹,並將其中序線索化,*Hpp指向頭結點 */ bool InOrderThreading(BThrNodePtr *Hpp, BThrNodePtr Tp) { cout << "InOrderThreading ..." << endl; *Hpp = (BThrNodePtr)malloc(sizeof(BThrNode)); if (!(*Hpp)) exit(1); (*Hpp)->LTag = Link;/* 建頭結點 */ (*Hpp)->RTag = Thread; (*Hpp)->RChild = (*Hpp);/* 右指標回指 */ if (!Tp) (*Hpp)->LChild = *Hpp;/* 若二叉樹空,則左指標回指 */ else { (*Hpp)->LChild = Tp; /* 綁定的線1 */ prev = (*Hpp); /* 頭結點是第一個走過的點*/ InThreading(Tp); /* 中序遍曆進行中序線索化 */ prev->RChild = *Hpp; /* 最後一個結點的後繼指向頭結點,即的線4*/ prev->RTag = Thread; (*Hpp)->RChild = prev; /* 頭結點的後繼指向最後一個結點,即的線2*/ } } /* 中序遍曆二叉線索樹(頭結點)的非遞迴演算法 */ bool InOrderTraverse_Thr(BThrNodePtr Hp) { cout << "InOrderTraverse ..." << endl; BThrNodePtr Bp; Bp = Hp->LChild;/* Bp指向根結點 */ while (Bp != Hp) { /* 空樹或遍曆結束時,Bp== Hp */ while (Bp->LTag == Link) Bp = Bp->LChild; /* 訪問其左子樹為空白的結點 */ cout << Bp->data << ' '; while (Bp->RTag == Thread && Bp->RChild != Hp) { Bp = Bp->RChild; cout << Bp->data << ' '; /* 訪問後繼結點 */ } Bp = Bp->RChild; } return true; } int main(void) { BThrNodePtr Hp, Tp; StrAssign(str, "ABDH##I##EJ###CF##G##"); cout << "輸入前序走訪序列 :" << endl; cout << str << endl; CreateBThrTree(&Tp); InOrderThreading(&Hp, Tp); InOrderTraverse_Thr(Hp); return 0; } |
輸出為:
由於線索二叉樹充分利用了null 指標域的空間,又保證了建立時一次遍曆就可以持續受用的前驅後繼資訊,所以如果所用的二叉樹需要經常遍曆或尋找結點時需要某種遍曆序列中的前驅後繼,那麼採用線索二叉鏈表的儲存結構就是不錯的選擇。