工科生一枚,熱衷於底層技術開發,有強烈的好奇心,感興趣內容包括單片機,嵌入式Linux,Uboot等,歡迎學習交流!
愛好跑步,打籃球,睡覺。
歡迎加我QQ1500836631(備註CSDN),一起學習交流問題,分享各種學習資料,電子書籍,學習視頻等。
線索二叉樹的概念
當我們對普通的二叉樹進行遍歷時需要使用棧結構做重複性的操作。線索二叉樹不需要如此,在遍歷的同時,使用二叉樹中空閒的內存空間記錄某些結點的前趨和後繼元素的位置(不是全部)。這樣在算法後期需要遍歷二叉樹時,就可以利用保存的結點信息,提高了遍歷的效率。使用這種方法構建的二叉樹,即爲“線索二叉樹”。
線索二叉樹的結構
每一棵二叉樹上,很多結點都含有未使用的指向NULL 的指針域。除了度爲2 的結點,度爲1 的結點,有一個空的指針域;葉子結點兩個指針域都爲NULL。
線索二叉樹實際上就是使用這些空指針域來存儲結點之間前趨和後繼關係的一種特殊的二叉樹。線索二叉樹中,如果結點有左子樹,則lchild 指針域指向左孩子,否則lchild 指針域指向該結點的直接前趨;同樣,如果結點有右子樹,則rchild 指針域指向右孩子,否則rchild 指針域指向該結點的直接後繼。
LTag 和RTag 爲標誌域。實際上就是兩個布爾類型的變量:
LTag 值爲0 時,表示lchild 指針域指向的是該結點的左孩子;爲1 時,表示指向的是該結點的直接前趨結點;
RTag 值爲0 時,表示rchild 指針域指向的是該結點的右孩子;爲1 時,表示指向的是該結點的直接後繼結點。
結點結構代碼實現:
#define TElemType int//宏定義,結點中數據域的類型
//枚舉,Link 爲0,Thread 爲1
typedef enum PointerTag{
Link,
Thread
}PointerTag;
//結點結構構造
typedef struct BiThrNode{
TElemType data;//數據域
struct BiThrNode* lchild,*rchild;//左孩子,右孩子指針域
PointerTag Ltag,Rtag;//標誌域,枚舉類型
}BiThrNode,*BiThrTree;
二叉樹的線索化
將二叉樹轉化爲線索二叉樹,實質上是在遍歷二叉樹的過程中,將二叉鏈表中的空指針改爲指向直接前趨或者直接後繼的線索。
線索化的過程即爲在遍歷的過程中修改空指針的過程。在遍歷過程中,如果當前結點沒有左孩子,需要將該結點的lchild 指針指向遍歷過程中的前一個結點,所以在遍歷過程中,設置一個指針(名爲pre ),時刻指向當前訪問結點的前一個結點。
代碼實現(拿中序遍歷爲例):
/**
* @Description: 中序對二叉樹進行線索化
* @Param: BiThrTree p 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InThreading(BiThrTree p)
{
//如果當前結點存在
if (p)
{
//遞歸當前結點的左子樹,進行線索化
InThreading(p->lchild);
//如果當前結點沒有左孩子,左標誌位設爲1,左指針域指向上一結點pre
if (!p->lchild)
{
//前驅線索
p->Ltag = Thread;
//左孩子指針指向前驅
p->lchild = pre;
}
//如果pre 沒有右孩子,右標誌位設爲1,右指針域指向當前結點。
if (pre && !pre->rchild)
{
//後繼線索
pre->Rtag = Thread;
//前驅右孩子指針指向後繼(當前結點p)
pre->rchild = p;
}
pre = p; //pre 指向當前結點
InThreading(p->rchild); //遞歸右子樹進行線索化
}
}
注意:中序對二叉樹進行線索化的過程中,在兩個遞歸函數中間的運行程序,和之前介紹的中序遍歷二叉樹的輸出函數的作用是相同的。將中間函數移動到兩個遞歸函數之前,就變成了前序對二叉樹進行線索化的過程;後序線索化同樣如此。
線索二叉樹進行遍歷
下圖中是一個按照中序遍歷建立的線索二叉樹。其中,實線表示指針,指向的是左孩子或者右孩子。虛線表示線索,指向的是該結點的直接前趨或者直接後繼。
使用線索二叉樹時,會經常遇到一個問題,如上圖中,結點8的直接後繼直接通過指針域獲得,爲結點5;而由於結點5的度爲2 ,無法利用指針域指向後繼結點,整個鏈表斷掉了。當在遍歷過程,遇到這種問題是解決的辦法就是:尋找先序、中序、後序遍歷的規律,找到下一個結點。
在先序遍歷過程中,如果結點因爲有右孩子導致無法找到其後繼結點,如果結點有左孩子,則後繼結點是其左孩子;否則,就一定是右孩子。拿上圖舉例,結點2的後繼結點是其左孩子結點4 ,如果結點4不存在的話,就是結點5 。
在中序遍歷過程中,結點的後繼是遍歷其右子樹時訪問的第一個結點,也就是右子樹中位於最左下的結點。例如上圖中結點5,後繼結點爲結點11,是其右子樹中位於最左邊的結點。反之,結點的前趨是左子樹最後訪問的那個結點。
後序遍歷中找後繼結點需要分爲3 種情況:
1. 如果該結點是二叉樹的根,後繼結點爲空;
2. 如果該結點是父結點的右孩子(或者是左孩子,但是父結點沒有右孩子),後繼結點是父結點;
3. 如果該結點是父結點的左孩子,且父結點有右子樹,後繼結點爲父結點的右子樹在後序遍歷列出的第一個結點。
使用後序遍歷建立的線索二叉樹,在真正使用過程中遇到鏈表的斷點時,需要訪問父結點,所以在初步建立二叉樹時,宜採用三叉鏈表做存儲結構。
遍歷線索二叉樹非遞歸代碼實現:
/**
* @Description: 中序遍歷線索二叉樹 非遞歸
* @Param: BiThrTree p 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree p)
{
while (p)
{
//一直找左孩子,最後一個爲中序序列中排第一的
while (p->Ltag == Link)
{
p = p->lchild;
}
//此時p指向中序遍歷序列的第一個結點(最左下的結點)
//打印(訪問)其左子樹爲空的結點
printf("%c ", p->data);
//當結點右標誌位爲1 時,直接找到其後繼結點
while (p->Rtag == Thread && p->rchild != NULL)
{
p = p->rchild;
//訪問後繼結點
printf("%c ", p->data);
}
//當p所指結點的rchild指向的是孩子結點而不是線索時,p的後繼應該是其右子樹的最左下的結點,即遍歷其右子樹時訪問的第一個節點
p = p->rchild;
}
}
完整代碼如下:
/*
* @Description: 線索二叉樹
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-05-20 16:31:33
* @LastEditors: Carlos
* @LastEditTime: 2020-06-01 20:19:22
*/
#include <stdio.h>
#include <stdlib.h>
#define TElemType char //宏定義,結點中數據域的類型
//枚舉,Link 爲0,Thread 爲1
typedef enum
{
Link,
Thread
} PointerTag;
//結點結構構造
typedef struct BiThrNode
{
//數據域
TElemType data;
//左孩子,右孩子指針域
struct BiThrNode *lchild, *rchild;
//標誌域,枚舉類型
PointerTag Ltag, Rtag;
} BiThrNode, *BiThrTree;
BiThrTree pre = NULL;
/**
* @Description: 初始化二叉樹
* @Param: BiTree *T 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void CreateBiTree(BiThrTree *T){
*T=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->data=1;
(*T)->lchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->rchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->lchild->data=2;
(*T)->lchild->lchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->lchild->rchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->lchild->rchild->data=5;
(*T)->lchild->rchild->lchild=NULL;
(*T)->lchild->rchild->rchild=NULL;
(*T)->rchild->data=3;
(*T)->rchild->lchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->rchild->lchild->data=6;
(*T)->rchild->lchild->lchild=NULL;
(*T)->rchild->lchild->rchild=NULL;
(*T)->rchild->rchild=(BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->rchild->rchild->data=7;
(*T)->rchild->rchild->lchild=NULL;
(*T)->rchild->rchild->rchild=NULL;
(*T)->lchild->lchild->data=4;
(*T)->lchild->lchild->lchild=NULL;
(*T)->lchild->lchild->rchild=NULL;
}
/**
* @Description: 採用前序初始化二叉樹 中序和後序只需改變賦值語句的位置即可
* @Param: BiThrTree *tree 二叉樹的結構體指針數組
* @Return: 無
* @Author: Carlos
*/
void CreateTree(BiThrTree *tree)
{
char data;
scanf("%c", &data);
if (data != '#')
{
if (!((*tree) = (BiThrNode *)malloc(sizeof(BiThrNode))))
{
printf("申請結點空間失敗");
return;
}
else
{
//採用前序遍歷方式初始化二叉樹
(*tree)->data = data;
//初始化左子樹
CreateTree(&((*tree)->lchild));
//初始化右子樹
CreateTree(&((*tree)->rchild));
}
}
else
{
*tree = NULL;
}
}
/**
* @Description: 中序對二叉樹進行線索化
* @Param: BiThrTree p 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InThreading(BiThrTree p)
{
//如果當前結點存在
if (p)
{
//遞歸當前結點的左子樹,進行線索化
InThreading(p->lchild);
//如果當前結點沒有左孩子,左標誌位設爲1,左指針域指向上一結點pre
if (!p->lchild)
{
//前驅線索
p->Ltag = Thread;
//左孩子指針指向前驅
p->lchild = pre;
}
//如果pre 沒有右孩子,右標誌位設爲1,右指針域指向當前結點。
if (pre && !pre->rchild)
{
//後繼線索
pre->Rtag = Thread;
//前驅右孩子指針指向後繼(當前結點p)
pre->rchild = p;
}
pre = p; //pre 指向當前結點
InThreading(p->rchild); //遞歸右子樹進行線索化
}
}
/**
* @Description: 中序遍歷線索二叉樹 非遞歸
* @Param: BiThrTree p 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree p)
{
while (p)
{
//一直找左孩子,最後一個爲中序序列中排第一的
while (p->Ltag == Link)
{
p = p->lchild;
}
//此時p指向中序遍歷序列的第一個結點(最左下的結點)
//打印(訪問)其左子樹爲空的結點
printf("%c ", p->data);
//當結點右標誌位爲1 時,直接找到其後繼結點
while (p->Rtag == Thread && p->rchild != NULL)
{
p = p->rchild;
//訪問後繼結點
printf("%c ", p->data);
}
//當p所指結點的rchild指向的是孩子結點而不是線索時,p的後繼應該是其右子樹的最左下的結點,即遍歷其右子樹時訪問的第一個節點
p = p->rchild;
}
}
int main()
{
BiThrTree t;
printf("輸入前序二叉樹:\n");
CreateTree(&t);
// CreateBiTree(&t);
InThreading(t);
printf("輸出中序序列:\n");
InOrderThraverse_Thr(t);
return 0;
}
雙向線索二叉樹的概念
在遍歷使用中序序列創建的線索二叉樹時,對於其中的每個結點,即使沒有線索的幫助
下,也可以通過中序遍歷的規律找到直接前趨和直接後繼結點的位置。也就是說,建立的線索二叉鏈表可以從兩個方向對結點進行中序遍歷。線索二叉鏈表可以從第一個結點往後逐個遍歷。但是起初由於沒有記錄中序序列中最後一個結點的位置,所以不能實現從最後一個結點往前逐個遍歷。雙向線索鏈表的作用就是可以讓線索二叉樹從兩個方向實現遍歷。
雙向線索二叉樹的實現過程
在線索二叉樹的基礎上,額外添加一個結點。此結點的作用類似於鏈表中的頭指針,數據域不起作用,只利用兩個指針域(由於都是指針,標誌域都爲0 )。左指針域指向二叉樹的樹根,確保可以正方向對二叉樹進行遍歷;同時,右指針指向線索二叉樹形成的線性序列中的最後一個結點。
這樣,二叉樹中的線索鏈表就變成了雙向線索鏈表,既可以從第一個結點通過不斷地找後繼結點進行遍歷,也可以從最後一個結點通過不斷找前趨結點進行遍歷。
代碼實現
/**
* @Description: 建立雙向線索鏈表
* @Param: BiThrTree *h 結構體指針數組 BiThrTree t 結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThread_Head(BiThrTree *h, BiThrTree t)
{
//初始化頭結點
(*h) = (BiThrTree)malloc(sizeof(BiThrNode));
if ((*h) == NULL)
{
printf("申請內存失敗");
return;
}
(*h)->rchild = *h;
(*h)->Rtag = Link;
//如果樹本身是空樹
if (!t)
{
(*h)->lchild = *h;
(*h)->Ltag = Link;
}
else
{
//pre 指向頭結點
pre = *h;
//頭結點左孩子設爲樹根結點
(*h)->lchild = t;
(*h)->Ltag = Link;
//線索化二叉樹,pre 結點作爲全局變量,線索化結束後,pre 結點指向中序序列中最後一個結點
InThreading(t);
//鏈接最後一個節點(最右下角G節點)和頭結點
pre->rchild = *h;
pre->Rtag = Thread;
//將頭結點的右指針指向中序序列最後一個節點
(*h)->rchild = pre;
}
}
雙向線索二叉樹的遍歷
雙向線索二叉樹遍歷時,如果正向遍歷,就從樹的根結點開始。整個遍歷過程結束的標誌是:當從頭結點出發,遍歷回頭結點時,表示遍歷結束。
/**
* @Description: 中序正向遍歷雙向線索二叉樹
* @Param: BiThrTree h 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
//p 指向根結點
p = h->lchild;
while (p != h)
{
//當ltag = 0 時循環到中序序列的第一個結點。
while (p->Ltag == Link)
{
p = p->lchild;
}
//顯示結點數據,可以更改爲其他對結點的操作
printf("%c ", p->data);
//如果當前節點經過了線索化,直接利用該節點訪問下一節點
while (p->Rtag == Thread && p->rchild != h)
{
p = p->rchild;
printf("%c ", p->data);
}
//如果沒有線索化或者跳出循環,說明其含有右子樹。p 進入其右子樹
p = p->rchild;
}
}
逆向遍歷線索二叉樹的過程即從頭結點的右指針指向的結點出發,逐個尋找直接前趨結點,結束標誌同正向遍歷一樣:
/**
* @Description: 中序逆方向遍歷線索二叉樹 和正向的區別在於 p = p->rchild 。逆向遍歷我們要一直訪問到右子樹的最後一個。
* @Param: BiThrTree h 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
p = h->rchild;
while (p != h)
{
while (p->Rtag == Link)
{
p = p->rchild;
}
printf("%c", p->data);
//如果lchild 爲線索,直接使用,輸出
while (p->Ltag == Thread && p->lchild != h)
{
p = p->lchild;
printf("%c", p->data);
}
p = p->lchild;
}
}
完整代碼如下
/*
* @Description: 雙向線索二叉樹的遍歷
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-06-01 20:46:38
* @LastEditors: Carlos
* @LastEditTime: 2020-06-01 21:17:23
*/
#include <stdio.h>
#include <stdlib.h>
//宏定義,結點中數據域的類型
#define TElemType char
//枚舉,Link 爲0,Thread 爲1
typedef enum
{
Link,
Thread
} PointerTag;
//結點結構構造
typedef struct BiThrNode
{
//數據域
TElemType data;
//左孩子,右孩子指針域
struct BiThrNode *lchild, *rchild;
//標誌域,枚舉類型
PointerTag Ltag, Rtag;
} BiThrNode, *BiThrTree;
BiThrTree pre = NULL;
/**
* @Description: 採用前序初始化二叉樹 中序和後序只需改變賦值語句的位置即可
* @Param: BiThrTree *tree 二叉樹的結構體指針數組
* @Return: 無
* @Author: Carlos
*/
void CreateTree(BiThrTree *tree)
{
char data;
scanf("%c", &data);
if (data != '#')
{
if (!((*tree) = (BiThrNode *)malloc(sizeof(BiThrNode))))
{
printf("申請結點空間失敗");
return;
}
else
{
//採用前序遍歷方式初始化二叉樹
(*tree)->data = data;
//初始化左子樹
CreateTree(&((*tree)->lchild));
//初始化右子樹
CreateTree(&((*tree)->rchild));
}
}
else
{
*tree = NULL;
}
}
/**
* @Description: 中序對二叉樹進行線索化
* @Param: BiThrTree p 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InThreading(BiThrTree p)
{
//如果當前結點存在
if (p)
{
//遞歸當前結點的左子樹,進行線索化
InThreading(p->lchild);
//如果當前結點沒有左孩子,左標誌位設爲1,左指針域指向上一結點pre
if (!p->lchild)
{
p->Ltag = Thread;
//pre爲頭結點,鏈接中序遍歷的第一個節點(最左邊的結點)與頭結點
p->lchild = pre;
}
//如果pre 沒有右孩子,右標誌位設爲1,右指針域指向當前結點。
if (pre && !pre->rchild)
{
pre->Rtag = Thread;
pre->rchild = p;
}
//pre 指向當前結點
pre = p;
//遞歸右子樹進行線索化
InThreading(p->rchild);
}
}
/**
* @Description: 建立雙向線索鏈表
* @Param: BiThrTree *h 結構體指針數組 BiThrTree t 結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThread_Head(BiThrTree *h, BiThrTree t)
{
//初始化頭結點
(*h) = (BiThrTree)malloc(sizeof(BiThrNode));
if ((*h) == NULL)
{
printf("申請內存失敗");
return;
}
(*h)->rchild = *h;
(*h)->Rtag = Link;
//如果樹本身是空樹
if (!t)
{
(*h)->lchild = *h;
(*h)->Ltag = Link;
}
else
{
//pre 指向頭結點
pre = *h;
//頭結點左孩子設爲樹根結點
(*h)->lchild = t;
(*h)->Ltag = Link;
//線索化二叉樹,pre 結點作爲全局變量,線索化結束後,pre 結點指向中序序列中最後一個結點
InThreading(t);
//鏈接最後一個節點(最右邊下角)和頭結點
pre->rchild = *h;
pre->Rtag = Thread;
(*h)->rchild = pre;
}
}
/**
* @Description: 中序正向遍歷雙向線索二叉樹
* @Param: BiThrTree h 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
//p 指向根結點
p = h->lchild;
while (p != h)
{
//當ltag = 0 時循環到中序序列的第一個結點。
while (p->Ltag == Link)
{
p = p->lchild;
}
//顯示結點數據,可以更改爲其他對結點的操作
printf("%c ", p->data);
//如果當前節點經過了線索化,直接利用該節點訪問下一節點
while (p->Rtag == Thread && p->rchild != h)
{
p = p->rchild;
printf("%c ", p->data);
}
//如果沒有線索化或者跳出循環,說明其含有右子樹。p 進入其右子樹
p = p->rchild;
}
}
/**
* @Description: 中序逆方向遍歷線索二叉樹 和正向的區別在於 p = p->rchild 。逆向遍歷我們要一直訪問到右子樹的最後一個。
* @Param: BiThrTree h 二叉樹的結構體指針
* @Return: 無
* @Author: Carlos
*/
void InOrderThraverse_Thr(BiThrTree h)
{
BiThrTree p;
p = h->rchild;
while (p != h)
{
while (p->Rtag == Link)
{
p = p->rchild;
}
printf("%c", p->data);
//如果lchild 爲線索,直接使用,輸出
while (p->Ltag == Thread && p->lchild != h)
{
p = p->lchild;
printf("%c", p->data);
}
p = p->lchild;
}
}
int main()
{
BiThrTree t;
BiThrTree h;
printf("輸入前序二叉樹:\n");
CreateTree(&t);
InOrderThread_Head(&h, t);
printf("輸出中序序列:\n");
InOrderThraverse_Thr(h);
return 0;
}
總結
二叉樹的線索化就是充分利用了節點的空指針域。所有節點線索化的過程也就是當前節點指針和上一結點指針進行鏈接的過程,不斷遞歸所有節點。線索化二叉樹的訪問,以中序遍歷爲例,首先需要訪問到中序遍歷的第一個節點,若當前節點進行了線索化,可以直接利用該節點進行下一節點的訪問。否則,說明當前節點含有右子樹,則進入右子樹進行訪問。雙向線索二叉樹的建立,其實就將頭結點的左指針和樹的根節點鏈接,頭結點的右指針和中序遍歷的最後一個節點的鏈接。這樣我們就可以進行雙向訪問了。當從頭結點出發,遍歷回頭結點時,表示遍歷結束。