@[TOC]算法
當咱們對普通的二叉樹進行遍歷時須要使用棧結構作重複性的操做。線索二叉樹不須要如此,在遍歷的同時,使用二叉樹中空閒的內存空間記錄某些結點的前趨和後繼元素的位置(不是所有)。這樣在算法後期須要遍歷二叉樹時,就能夠利用保存的結點信息,提升了遍歷的效率。使用這種方法構建的二叉樹,即爲「線索二叉樹」。數組
每一棵二叉樹上,不少結點都含有未使用的指向NULL 的指針域。除了度爲2 的結點,度爲1 的結點,有一個空的指針域;葉子結點兩個指針域都爲NULL。函數
線索二叉樹實際上就是使用這些空指針域來存儲結點之間前趨和後繼關係的一種特殊的二叉樹。線索二叉樹中,若是結點有左子樹,則lchild 指針域指向左孩子,不然lchild 指針域指向該結點的直接前趨;一樣,若是結點有右子樹,則rchild 指針域指向右孩子,不然rchild 指針域指向該結點的直接後繼。
LTag 和RTag 爲標誌域。實際上就是兩個布爾類型的變量:
LTag 值爲0 時,表示lchild 指針域指向的是該結點的左孩子;爲1 時,表示指向的是該結點的直接前趨結點;
RTag 值爲0 時,表示rchild 指針域指向的是該結點的右孩子;爲1 時,表示指向的是該結點的直接後繼結點。
結點結構代碼實現:spa
#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); //遞歸右子樹進行線索化 } }
注意:中序對二叉樹進行線索化的過程當中,在兩個遞歸函數中間的運行程序,和以前介紹的中序遍歷二叉樹的輸出函數的做用是相同的。將中間函數移動到兩個遞歸函數以前,就變成了前序對二叉樹進行線索化的過程;後序線索化一樣如此。code
下圖中是一個按照中序遍歷創建的線索二叉樹。其中,實線表示指針,指向的是左孩子或者右孩子。虛線表示線索,指向的是該結點的直接前趨或者直接後繼。
使用線索二叉樹時,會常常遇到一個問題,如上圖中,結點8的直接後繼直接經過指針域得到,爲結點5;而因爲結點5的度爲2 ,沒法利用指針域指向後繼結點,整個鏈表斷掉了。當在遍歷過程,遇到這種問題是解決的辦法就是:尋找先序、中序、後序遍歷的規律,找到下一個結點。
在先序遍歷過程當中,若是結點由於有右孩子致使沒法找到其後繼結點,若是結點有左孩子,則後繼結點是其左孩子;不然,就必定是右孩子。拿上圖舉例,結點2的後繼結點是其左孩子結點4 ,若是結點4不存在的話,就是結點5 。
在中序遍歷過程當中,結點的後繼是遍歷其右子樹時訪問的第一個結點,也就是右子樹中位於最左下的結點。例如上圖中結點5,後繼結點爲結點11,是其右子樹中位於最左邊的結點。反之,結點的前趨是左子樹最後訪問的那個結點。
後序遍歷中找後繼結點須要分爲3 種狀況:
1. 若是該結點是二叉樹的根,後繼結點爲空;
2. 若是該結點是父結點的右孩子(或者是左孩子,可是父結點沒有右孩子),後繼結點是父結點;
3. 若是該結點是父結點的左孩子,且父結點有右子樹,後繼結點爲父結點的右子樹在後序遍歷列出的第一個結點。
使用後序遍歷創建的線索二叉樹,在真正使用過程當中遇到鏈表的斷點時,須要訪問父結點,因此在初步創建二叉樹時,宜採用三叉鏈表作存儲結構。
遍歷線索二叉樹非遞歸代碼實現:blog
/** * @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 )。左指針域指向二叉樹的樹根,確保能夠正方向對二叉樹進行遍歷;同時,右指針指向線索二叉樹造成的線性序列中的最後一個結點。
這樣,二叉樹中的線索鏈表就變成了雙向線索鏈表,既能夠從第一個結點經過不斷地找後繼結點進行遍歷,也能夠從最後一個結點經過不斷找前趨結點進行遍歷。
代碼實現ip
/** * @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; }
二叉樹的線索化就是充分利用了節點的空指針域。全部節點線索化的過程也就是當前節點指針和上一結點指針進行連接的過程,不斷遞歸全部節點。線索化二叉樹的訪問,以中序遍歷爲例,首先須要訪問到中序遍歷的第一個節點,若當前節點進行了線索化,能夠直接利用該節點進行下一節點的訪問。不然,說明當前節點含有右子樹,則進入右子樹進行訪問。雙向線索二叉樹的創建,其實就將頭結點的左指針和樹的根節點連接,頭結點的右指針和中序遍歷的最後一個節點的連接。這樣咱們就能夠進行雙向訪問了。當從頭結點出發,遍歷回頭結點時,表示遍歷結束。
如遇到排版錯亂的問題,能夠經過如下連接訪問個人CSDN。
**CSDN:[CSDN搜索「嵌入式與Linux那些事」]