數據結構(15.3)線索二叉樹
前言
樹形結構是一種非線性結構,我們對二叉樹進行遍歷,是將二叉樹的結點按照一定的規則排列成一個線性序列,這實際上是對一個非線性化結構進行了線性化操作。前面提到過,線性結構除了第一個結點和最後一個結點以外,每一個結點都有且只有一個前驅和後繼。觀察我們的二叉樹可以發現,我們沒有是辦法直接在樹中找到每個結點的前驅和後繼的,這個信息只能在二叉樹的遍歷中得到。
與此同時,二叉樹中存在很多空的鏈域,造成了空間上的浪費。(有n個結點,就會有n+1個空鏈域)
爲了解決這些問題,有人想到可以使用空的鏈域來存放結點的前驅和後繼,於是線索二叉樹就出現了。
現在,二叉樹的子結點指向有兩種可能性:既可能指向其左子樹或者右子樹,也可能指向其前驅或者後繼。當結點指向的是前驅或者後繼時,稱結點爲線索。擁有線索的二叉樹就是線索二叉樹。把空指針轉化爲線索的過程,就叫做線索化。
注意:將二叉樹按照不同的規則來排列可以得到不同的序列,結點的前驅和後繼也會不同,這樣線索的指向也不同了。也就是說,對同一個二叉樹,按照不同的次序進行線索化,可以分別得到先序線索二叉樹、中序線索二叉樹和後序線索二叉樹。本文講的是中序線索的二叉樹。
線索二叉樹的存儲結構
我們知道,二叉樹的結點有三個部分,分別是數據域、左孩子和右孩子。當使用空指針來存放線索時,我們不知道左右孩子指針指向的是子樹還是線索。爲了解決這個問題,線索化二叉樹需要增加兩個標誌域,通過標誌來協助判斷。
//標記
typedef enum {
//子樹
LINK,
//線索
THREAD
}TagType;
//線索二叉樹的結點
typedef struct BinTreeNode{
//數據域
ElemType data;
//左孩子/線索
struct BinTreeNode *leftChild;
//右孩子/線索
struct BinTreeNode *rightChild;
//左標記
TagType leftTag;
//右標記
TagType rightTag;
}BinTreeNode;
線索二叉樹的初始化與創建
線索化二叉樹就是對二叉樹裏的空指針進行修改,使其指向結點前驅或者後繼,而前驅和後繼顯然無法在創建時就獲得。
這意味着要得到線索二叉樹,首先要存在一個二叉樹。因此在初始化和創建上,線索二叉樹與二叉樹是相同的。
創建是指通過輸入一串字符串來生成二叉樹,要有特殊的字符來表示結點不存在。因此初始化時要設置標記的值。
//初始化
void InitBinTree(BinTree *bt, ElemType ref){
//初始化樹根爲空
bt->root = NULL;
//設置結束標記
bt->refvalue = ref;
}
創建:
爲了方便,先寫一個生成結點的方法:
//生成一個結點
BinTreeNode *GetNewNode(ElemType x){
BinTreeNode *s = (BinTreeNode *)malloc(sizeof(BinTreeNode));
assert(s != NULL);
s->data = x;
s->leftChild = s->rightChild = NULL;
s->leftTag = s->rightTag = LINK;
return s;
}
然後實現創建方法:
//創建
void CreateBinTree(BinTree *bt, char *str){
CreateBinTree(bt, bt->root, str);
}
void CreateBinTree(BinTree *bt, BinTreeNode *&t, char *&str){
if (*str == bt->refvalue) {
t = NULL;
}else{
t = GetNewNode(*str);
CreateBinTree(bt, t->leftChild, ++str);
CreateBinTree(bt, t->rightChild, ++str);
}
}
注:這次的代碼是用c++來寫的,有些地方使用了引用,和直接用指針區別不大:
//創建
void CreateBinTree(BinTree *bt, char *str){
CreateBinTree1(bt, &(bt->root), str);
}
void CreateBinTree1(BinTree *bt, BinTreeNode **t,char *&str){
if (*str == bt->refvalue) {
t = NULL;
}else{
*t = GetNewNode(*str);
CreateBinTree1(bt, &(*t)->leftChild, ++str);
CreateBinTree1(bt, &(*t)->rightChild, ++str);
}
}
注注:字符串如果不使用引用,而是用char **str的話,我使用的編譯器會出現一些問題,因此這裏字符串還是使用的引用。
二叉樹的中序線索化
來比較一下中序遍歷方法和線索化方法:
中序遍歷:
//中序遍歷-遞歸
void InOrder(BinTreeNode *t){
if (t != NULL) {
//左子樹遞歸遍歷
InOrder(t->leftChild);
//輸出結點信息
printf("%4c",t->data);
//右子樹遞歸遍歷
InOrder(t->rightChild);
}
}
線索化:
void CreateInThread(BinTreeNode *&t, BinTreeNode *&pre){
if (t != NULL) {
//左子樹遞歸線索化
CreateInThread(t->leftChild, pre);
//將空指針變爲線索
if (t ->leftChild == NULL) {
//左子樹爲空->左子樹指向前驅結點
t->leftTag = THREAD;
t->leftChild = pre;
}
if (pre != NULL && pre->rightChild == NULL) {
//前驅結點的右子樹爲空->前驅結點的右子樹指向該結點(後繼)
pre->rightTag = THREAD;
pre->rightChild = t;
}
//更新前驅結點
pre = t;
//右子樹遞歸線索化
CreateInThread(t->rightChild, pre);
}
}
可以發現,線索化方法和中序遍歷方法在結構上是一致的,只不過是中間的操作有所區別。實際上,因爲結點的前驅或後繼信息只能在遍歷中得到,中序線索化的實質就是在中序遍歷的過程中,對空指針進行修改。
當我們找到一個子結點爲空的結點時,假如是左結點,那麼就要修改它的標記值,並且讓它指向本結點的前驅。這說明了我們需要一個額外的變量來保存前驅結點。
假如是右結點,我們此時是不知道本結點的後繼結點的。
但是注意:對本結點的前驅結點來說,本結點就是其後繼結點。也就是說我們這時候能得到前驅結點的後繼信息,因此可以判斷前驅結點的右結點是否爲空,假如爲空,則修改它的標記值,然後讓它指向本結點。
這時候本結點訪問完畢了,需要更新一下前驅結點的指向(本結點成爲了下一個結點的前驅結點)。這同樣也說明,對結點的右結點進行修改,是要放到下一個結點中處理的。
有人可能注意到,本結點的右結點放到下一個結點來操作,那麼到最後一個結點的右結點是不會被操作到的
這就需要進行特殊處理。
可以另外寫一個方法,用於調用CreateInThread()方法,同時對最後一個結點進行處理(我使用的是c++,所以兩個方法的名稱是一樣的)
//中序線索化
void CreateInThread(BinTree *bt){
BinTreeNode *pre = NULL;
//調用上面的線索化方法
CreateInThread(bt->root, pre);
//處理最後一個結點的右結點
pre->rightChild = NULL;
pre->rightTag = THREAD;
}
線索二叉樹的遍歷
現在我們有了一棵線索化的二叉樹,可以很容易得到某個結點的前驅和後繼信息,這樣遍歷起來也很容易。先看如何找到結點的前驅和後繼。
尋找前驅
對於某結點而言,如果其左結點是線索,就找到了它的前驅。那麼如果其左結點是子樹呢?通過觀察可以發現,如果左結點是子樹,那麼該結點的前驅就是遍歷該結點左子樹時最後一個訪問的結點(左子樹中最右下的結點)。
那麼,如何找到某個子樹的最後一個結點呢?我們知道這個結點在最右下方,那麼就一直往右子樹查找,只需要找到右結點標記值爲線索的結點,就說明走到頭了,該結點就是最後一個結點。
//查找最後一個結點
BinTreeNode *Last(BinTreeNode *&t){
if(t == NULL){
return NULL;
}
BinTreeNode *p = t;
while (p->rightTag == LINK) {
p = p->rightChild;
}
return p;
}
這樣,對尋找結點前驅來說,只需要判斷其左結點的標記值,如果是線索,則直接返回左結點;否則返回左子樹的最後一個結點。
//尋找前驅
BinTreeNode *Prio(BinTreeNode *t,BinTreeNode *cur){
if(t == NULL || cur == NULL){
return NULL;
}
if (cur->leftTag == THREAD) {
return cur->leftChild;
}else{
return Last(cur->leftChild);
}
}
尋找後繼
同理,對於某結點而言,假如其右結點是線索,可以直接得到後繼。假如是子樹,則其後繼是訪問右子樹時訪問到的第一個結點(右子樹中最左下的結點)。
查找某子樹中訪問到的第一個結點也是一樣,一直往左子樹查找,找到左結點的標記值爲線索的結點,就說明到頭了。
//查找第一個訪問到的結點
BinTreeNode *First(BinTreeNode *&t){
if(t == NULL){
return NULL;
}
BinTreeNode *p = t;
while (p->leftTag == LINK) {
p = p->leftChild;
}
return p;
}
對尋找後繼結點來說,只需要判斷其右結點的標記值,如果爲線索則直接返回右結點;否則返回右子樹的第一個結點。
//尋找後繼
BinTreeNode *Next(BinTreeNode *&t,BinTreeNode *cur){
if(t == NULL || cur == NULL){
return NULL;
}
//如果右子樹是線索,直接返回右右子樹
if (cur->rightTag == THREAD) {
return cur->rightChild;
}else{
return First(cur->rightChild);
}
}
遍歷
現在,要遍歷二叉樹就很容易了:只需要從第一個結點開始,不斷找到其後繼並且輸出即可。
//中序遍歷
void InOrder(BinTreeNode *t){
BinTreeNode *p;
for (p = First(t); p != NULL; p = Next(t, p)) {
printf("%c ",p->data);
}
printf("\n");
}
當然,這麼中序遍歷簡便是對於中序線索二叉樹來說的,如果其他遍歷也想這麼簡便,還需要建立對應次序的線索二叉樹。但是,在先序線索二叉樹上尋找前驅或者在後序線索二叉樹上尋找後繼,都比較複雜。
線索二叉樹的查找
查找某結點
要查找某結點很容易,只需要在遍歷的同時尋找即可
BinTreeNode *Search(BinTreeNode *t,ElemType key){
BinTreeNode *p = NULL;
for (p = First(t); p != NULL; p = Next(t, p)) {
if (p->data == key) {
return p;
}
}
return p;
}
查找某結點的父結點
查找父結點則有點困難。總的來說,想查找某個結點的父結點,要將以該結點爲根的樹看做一個整體,然後尋找到這棵樹的前驅和後繼結點,其中必然有一個是其父結點。所以還需要特別判斷:假如是前驅,判斷前驅的右子樹是不是本結點;假如是後繼,判斷後繼的左子樹是不是本結點。
首先觀察擁有線索的結點,以E結點爲例,以E爲根的整棵樹,其前驅結點是B,後繼結點是D,判斷一下前驅結點的右子樹和後繼結點的左子樹是否爲E,即可找到E的父結點。(擁有線索的結點,它的父結點一定是它的線索)。
假如結點擁有的是左右子樹,很顯然我們需要繞出這棵樹來尋找。以D結點爲例,將以D爲根的子樹看做一個整體,要繞出這棵樹可以選擇往左子樹一直走,繞到結點B,B此時可以看做以D爲根的整棵樹的前驅;也可以往右子樹一直走,繞到結點A,A此時可以看做以D爲根的整棵樹的後繼。A結點和B結點之中必然有一個是D的父結點。
總結以上規律,可以得到方法:
BinTreeNode *Parent(BinTreeNode *t,BinTreeNode *cur){
if (t == NULL || cur == NULL) {
return NULL;
}
if (t == cur) {
return NULL;
}
BinTreeNode *p;
//有線索->查找前驅
if (cur->leftTag == THREAD) {
p = cur->leftChild;
if (p->rightChild == cur) {
return p;
}
}
//有線索->查找後繼
if (cur->rightTag == THREAD) {
p = cur->rightChild;
if (p->leftChild == cur) {
return p;
}
}
//有子樹->得到整棵樹的前驅
p = First(cur->leftChild);
p = p->leftChild;
if (p != NULL && p->rightChild == cur) {
return p;
}
//得到整棵樹的後繼
p = Last(cur->rightChild);
//不是前驅必然是後繼,直接輸出
return p->rightChild;
}
全部代碼
ThreadBinTree.hpp
#ifndef ThreadBinTree_hpp
#define ThreadBinTree_hpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define ElemType char
//標記
typedef enum {
LINK,
THREAD
}TagType;
//線索二叉樹的結點
typedef struct BinTreeNode{
//數據域
ElemType data;
//左孩子/線索
struct BinTreeNode *leftChild;
//右孩子/線索
struct BinTreeNode *rightChild;
//左標記
TagType leftTag;
//右標記
TagType rightTag;
}BinTreeNode;
//線索二叉樹
typedef struct BinTree{
BinTreeNode *root;
ElemType refvalue;
}BinTree;
//初始化
void InitBinTree(BinTree *bt, ElemType ref);
//申請一個結點
BinTreeNode *GetNewNode(ElemType x);
//創建
void CreateBinTree(BinTree *bt, char *str);
void CreateBinTree(BinTree *bt, BinTreeNode *&t, char *&str);
void CreateBinTree1(BinTree *bt, BinTreeNode **t,char *&str);
//中序線索化
void CreateInThread(BinTree *bt);
void CreateInThread(BinTreeNode *&t, BinTreeNode *&pre);
void CreateInThread1(BinTreeNode *&t, BinTreeNode *&pre);
//求第一個結點
BinTreeNode *First(BinTree *bt);
BinTreeNode *First(BinTreeNode *&t);
//求最後一個結點
BinTreeNode *Last(BinTree *bt);
BinTreeNode *Last(BinTreeNode *&t);
//求後繼結點
BinTreeNode *Next(BinTree *bt,BinTreeNode *cur);
BinTreeNode *Next(BinTreeNode *&t,BinTreeNode *cur);
//求前驅結點
BinTreeNode *Prio(BinTree *bt,BinTreeNode *cur);
BinTreeNode *Prio(BinTreeNode *t,BinTreeNode *cur);
//中序遍歷
void InOrder(BinTree *bt);
void InOrder(BinTreeNode *t);
//查找某結點
BinTreeNode *Search(BinTree *bt,ElemType key);
BinTreeNode *Search(BinTreeNode *t,ElemType key);
//查找某結點的父結點
BinTreeNode *Parent(BinTree *bt, BinTreeNode *cur);
BinTreeNode *Parent(BinTreeNode *t,BinTreeNode *cur);
#endif /* ThreadBinTree_hpp */
ThreadBinTree.cpp
#include "ThreadBinTree.hpp"
//初始化
void InitBinTree(BinTree *bt, ElemType ref){
//初始化樹根爲空
bt->root = NULL;
//設置結束標記
bt->refvalue = ref;
}
//生成一個結點
BinTreeNode *GetNewNode(ElemType x){
BinTreeNode *s = (BinTreeNode *)malloc(sizeof(BinTreeNode));
assert(s != NULL);
s->data = x;
s->leftChild = s->rightChild = NULL;
s->leftTag = s->rightTag = LINK;
return s;
}
//創建
void CreateBinTree(BinTree *bt, char *str){
CreateBinTree1(bt, &(bt->root), str);
}
void CreateBinTree(BinTree *bt, BinTreeNode *&t, char *&str){
if (*str == bt->refvalue) {
t = NULL;
}else{
t = GetNewNode(*str);
CreateBinTree(bt, t->leftChild, ++str);
CreateBinTree(bt, t->rightChild, ++str);
}
}
void CreateBinTree1(BinTree *bt, BinTreeNode **t,char *&str){
if (*str == bt->refvalue) {
t = NULL;
}else{
*t = GetNewNode(*str);
CreateBinTree1(bt, &(*t)->leftChild, ++str);
CreateBinTree1(bt, &(*t)->rightChild, ++str);
}
}
//中序線索化
void CreateInThread(BinTree *bt){
BinTreeNode *pre = NULL;
CreateInThread(bt->root, pre);
//處理最後一個結點的右結點
pre->rightChild = NULL;
pre->rightTag = THREAD;
}
void CreateInThread(BinTreeNode *&t, BinTreeNode *&pre){
if (t != NULL) {
//左子樹遞歸線索化
CreateInThread(t->leftChild, pre);
//將空指針變爲線索
if (t ->leftChild == NULL) {
//左子樹爲空->左子樹指向前驅結點
t->leftTag = THREAD;
t->leftChild = pre;
}
if (pre != NULL && pre->rightChild == NULL) {
//前驅結點的右子樹爲空->前驅結點的右子樹指向該結點(後繼)
pre->rightTag = THREAD;
pre->rightChild = t;
}
//更新前驅結點
pre = t;
//右子樹遞歸線索化
CreateInThread(t->rightChild, pre);
}
}
void CreateInThread1(BinTreeNode *&t, BinTreeNode *&pre){
if (t) {
CreateInThread(t->leftChild, pre);
if (!t->leftChild) {
t->leftTag = THREAD;
t->leftChild = pre;
}
if (!pre || !pre->rightChild) {
pre->rightTag = THREAD;
pre->rightChild = t;
}
pre = t;
CreateInThread1(t->rightChild, pre);
}
}
//求第一個訪問到的結點
BinTreeNode *First(BinTree *bt){
return First(bt->root);
}
BinTreeNode *First(BinTreeNode *&t){
if(t == NULL){
return NULL;
}
BinTreeNode *p = t;
while (p->leftTag == LINK) {
p = p->leftChild;
}
return p;
}
//求最後一個訪問到的結點
BinTreeNode *Last(BinTree *bt){
return Last(bt->root);
}
BinTreeNode *Last(BinTreeNode *&t){
if(t == NULL){
return NULL;
}
BinTreeNode *p = t;
while (p->rightTag == LINK) {
p = p->rightChild;
}
return p;
}
//求後繼結點
BinTreeNode *Next(BinTree *bt,BinTreeNode *cur){
return Next(bt->root, cur);
}
BinTreeNode *Next(BinTreeNode *&t,BinTreeNode *cur){
if(t == NULL || cur == NULL){
return NULL;
}
//如果右子樹是線索,直接返回右右子樹
if (cur->rightTag == THREAD) {
return cur->rightChild;
}else{
return First(cur->rightChild);
}
}
//求前驅結點
BinTreeNode *Prio(BinTree *bt,BinTreeNode *cur){
return Prio(bt->root, cur);
}
BinTreeNode *Prio(BinTreeNode *t,BinTreeNode *cur){
if(t == NULL || cur == NULL){
return NULL;
}
if (cur->leftTag == THREAD) {
return cur->leftChild;
}else{
return Last(cur->leftChild);
}
}
//中序遍歷
void InOrder(BinTree *bt){
InOrder(bt->root);
}
void InOrder(BinTreeNode *t){
BinTreeNode *p;
for (p = First(t); p != NULL; p = Next(t, p)) {
printf("%c ",p->data);
}
printf("\n");
}
//查找某結點
BinTreeNode *Search(BinTree *bt,ElemType key){
return Search(bt->root, key);
}
BinTreeNode *Search(BinTreeNode *t,ElemType key){
BinTreeNode *p = NULL;
for (p = First(t); p != NULL; p = Next(t, p)) {
if (p->data == key) {
return p;
}
}
return p;
}
//查找某結點的父結點
BinTreeNode *Parent(BinTree *bt, BinTreeNode *cur){
return Parent(bt->root, cur);
}
BinTreeNode *Parent(BinTreeNode *t,BinTreeNode *cur){
if (t == NULL || cur == NULL) {
return NULL;
}
if (t == cur) {
return NULL;
}
BinTreeNode *p;
//有線索->查找前驅
if (cur->leftTag == THREAD) {
p = cur->leftChild;
if (p->rightChild == cur) {
return p;
}
}
//有線索->查找後繼
if (cur->rightTag == THREAD) {
p = cur->rightChild;
if (p->leftChild == cur) {
return p;
}
}
//有子樹->得到整棵樹的前驅
p = First(cur->leftChild);
p = p->leftChild;
if (p != NULL && p->rightChild == cur) {
return p;
}
//得到整棵樹的後繼
p = Last(cur->rightChild);
//不是前驅必然是後繼,直接輸出
return p->rightChild;
}
main.cpp
#include "ThreadBinTree.hpp"
int main(int argc, const char * argv[]) {
char *str = "ABC##DE##F##G#H##";
BinTree myTree;
InitBinTree(&myTree,'#');
CreateBinTree(&myTree, str);
CreateInThread(&myTree);
InOrder(&myTree);
BinTreeNode *first = First(&myTree);
printf("first:%c\n",first->data);
BinTreeNode *last = Last(&myTree);
printf("last:%c\n",last->data);
BinTreeNode *find = Search(&myTree, 'D');
printf("find 'D':%c\n",find->data);
BinTreeNode *pre = Prio(&myTree,find);
printf("D pre:%c\n",pre->data);
BinTreeNode *next = Next(&myTree,find);
printf("D next:%c\n",next->data);
BinTreeNode *parent = Parent(&myTree, find);
printf("parent: %c\n",parent->data);
return 0;
}