定义:https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%B4%A2%E4%BA%8C%E5%8F%89%E6%A0%91
线索二叉树(引线二叉树) 的定义如下:
“一个二叉树通过如下的方法“穿起来”:所有原本为空的右(孩子)指针改为指向该节点在中序序列中的后继,所有原本为空的左(孩子)指针改为指向该节点的中序序列的前驱。”[1]
线索二叉树能线性地遍历二叉树,从而比递归的 中序遍历更快。使用线索二叉树也能够方便的找到一个节点的父节点,这比显式地使用父亲节点指针或者栈效率更高。这在栈空间有限,或者无法使用存储父节点的栈时很有作用(对于通过深度优先搜索来查找父节点而言)。 考虑这样的例子:一个节点k有一个右孩子r,那么r的左指针可能是指向一个孩子节点,或是一个指回k的线索。如果r有左孩子,这个左孩子同样也应该有一个左孩子或是指回k的线索。对于所有的左孩子同理。因此沿着这些从r发出的左指针,我们最终会找到一个指回k的线索。这种特性是对称的:当q是p的左孩子时,我们可以沿着q的右孩子找到一个指回p的线索。
传统的二叉树一般都是以链式存储的结构来表示。这样,二叉树中的每个节点都可以用链表中的一个链节点来存储,每个链节点就包含了若干个指针。但是,这种传统的链式存储结构只能表现出二叉树中节点之间的父子关系,而且不能利用空余的指针来直接得到某个节点的在特定的遍历顺序(先序,中序,后序)中的直接前驱和直接后继。通过分析传统的二叉树链式存储结构表示的二叉树中,存在大量的空闲指针。若能利用这些空指针域来存放指向该节点的直接前驱或是直接后继的指针,则可以进行某些更方便的运算。这些被重新利用起来的空指针就被称为线索,加上了这些线索的二叉树就是线索二叉树
目地:
对二叉树以某种遍历顺序进行扫描并为每个节点添加线索的过程称为二叉树的线索化,进行线索化的目的是为了加快查找二叉树中某节点的前驱和后继的速度。 那么在有N个节点的二叉树中可以利用N+1个空指针添加线索。这是因为在N个节点的二叉树中,每个节点有2个指针,所以一共有2N个指针,除了根节点以外每一个节点都有一个指针从它的父节点指向它,所以一共使用了N-1个指针。所以剩下2N-(N-1)个空指针。
代码:
结构体定义:
struct ThreadNode
{
int ltag; //ltar为0表示lchild指向左子女,为1表示lchild为中序前驱
int rtag; //rtag同上
int data;
struct ThreadNode *lchild,*rchild;
};
1、建立中序线索二叉树
建立线索二叉树之前需要使用先建立二叉树,建立时将ltag
和rtag
初始化为0
,然后重新用中序遍历的方法遍历二叉树,在遍历过程中去检查该结点是否有左右子女。若无左子女,其ltag
设为1
,并用其lchild
指针域指向其在中序遍历时的前驱pre
(可设为全局变量或作函数参数,用于保存当前节点的前驱,中序遍历的第一节点的前驱设NULL);若无右子女,则其rtag
设为1
,rchild
指针域指向后继结点。
先创建二叉树(这里使用给定先序遍历和中序遍历生成二叉树的方法https://mp.csdn.net/mdeditor/102650404#
,其他方法也可,只要在给结点赋值的时候,将ltag
和rtag
初始化为0
即可)
void createBinTreeUsePreAndIn(ThreadNode *&root,char pre[],char in[],int a,int b,int c,int d)
{//由二叉树的先序序列和中序序列构建二叉树
if(a <= b)
{
root = new ThreadNode(); //创建根节点
root->data = pre[a];
root->lchild = NULL;
root->rchild = NULL;
root->ltag = 0;
root->rtag = 0;
int i;
for(i = c; i <= d; i++) //中序序列中查找根的位置,此处为i
{
if(in[i] == pre[a])
break;
}
createBinTreeUsePreAndIn(root->lchild,pre,in,a+1,a+i-c,c,i-1); //递归建左子树
createBinTreeUsePreAndIn(root->rchild,pre,in,a+i-c+1,b,i+1,d); //递归建右子树
}
}
然后,遍历生成的二叉树,给空指针域加线索:
void inThreaded(ThreadNode *p,ThreadNode *&pre)
{
if(p != NULL)
{
inThreaded(p->lchild,pre); //递归,左子树线索化
if(p->lchild == NULL) //建立当前结点的前驱线索
{
p->lchild = pre;
p->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL) //建立前驱结点的后驱线索
{
pre->rchild = p;
pre->rtag = 1;
}
pre = p; //前驱跟上,当前指针向前遍历
inThreaded(p->rchild,pre); //递归,右子树线索化
}
}
利用中序遍历对二叉树进行中序线索化的主过程
void createThreadTree_in(ThreadNode* root)
{
ThreadNode *pre = NULL; //前驱结点指针
if(root != NULL) //非空二叉树,线索化
{
inThreaded(root,pre); //中序遍历线索化二叉树
pre->lchild = NULL; //后处理中序最后一个结点
pre->rtag = 1;
}
}
2、访问:
因为线索二叉树只是利用了没用使用的空指针域来当线索,对于有左右子女的结点来说,它们的lchild
和rchild
仍然指向其左右子女,而不是它们的前驱和后继(没有左或者右子女的结点的左右子女指针域指向前驱或者后继)。所以访问使用要检查ltag
和rtag
,通过他们的值来判断。
寻找*t
为根结点在中序遍历时第一个访问的结点 前提:建立了中序线索二叉树
ThreadNode* inFirst(ThreadNode* t)
{
while(t->ltag == 0) t = t->lchild;
return t;
}
寻找*t
结点在中序遍历时下一个访问的结点 前提:建立了中序线索二叉树
ThreadNode* inNext(ThreadNode* t)
{
if(t->rtag == 0) return inFirst(t->rchild); //中序后继是其右子树下最左下的结点
else return t->rchild; //rtag = 1,直接返回后继线索
}
使用中序线索二叉树进行中序遍历
void inOrder(ThreadNode* root)
{
for(ThreadNode *p = inFirst(root); p != NULL; p = inNext(p))
{
cout<<(char)(p->data);
}
cout<<endl;
}
扩展:
中序下最后一个结点:
ThreadNode* inLast(ThreadNode* t)
{
while(t->rtag == 0) t = t->rchild;
return t;
}
前一个结点:
ThreadNode* pre(ThreadNode* t)
{
if(t->ltag == 0) return inLast(t->lchild); //中序前驱是其左子树下最右下的结点
else return t->lchild; //rtag = 1,直接返回前驱线索
}
3、完整代码:
#include<iostream>
using namespace std;
struct ThreadNode
{
int ltag; //ltar为0表示lchild指向左子女,为1表示lchild为中序前驱
int rtag; //rtag同上
int data;
struct ThreadNode *lchild,*rchild;
};
ThreadNode* inFirst(ThreadNode* t) //寻找t结点在中序遍历时第一个访问的结点
{ //前提:建立了中序线索二叉树
while(t->ltag == 0) t = t->lchild;
return t;
}
ThreadNode* inNext(ThreadNode* t) //寻找t结点在中序遍历时下一个访问的结点 //前提:建立了中序线索二叉树
{ //前提:建立了中序线索二叉树
if(t->rtag == 0) return inFirst(t->rchild); //中序后继是其右子树下最左下的结点
else return t->rchild; //rtag = 1,直接返回后继线索
}
ThreadNode* inLast(ThreadNode* t) //寻找t结点在中序遍历时最后一个访问的结点
{ //前提:建立了中序线索二叉树
while(t->rtag == 0) t = t->rchild;
return t;
}
ThreadNode* pre(ThreadNode* t) //寻找t结点在中序遍历时上一个访问的结点 //前提:建立了中序线索二叉树
{ //前提:建立了中序线索二叉树
if(t->ltag == 0) return inLast(t->lchild); //中序后继是其左子树下最右下的结点
else return t->lchild; //rtag = 1,直接返回前继线索
}
void inOrder(ThreadNode* root)
{
for(ThreadNode *p = inFirst(root); p != NULL; p = inNext(p))
{
cout<<(char)(p->data)<<"\t";
}
cout<<endl;
}
void inThreaded(ThreadNode *p,ThreadNode *&pre)
{
if(p != NULL)
{
inThreaded(p->lchild,pre); //递归,左子树线索化
if(p->lchild == NULL) //建立当前结点的前驱线索
{
p->lchild = pre;
p->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL) //建立前驱结点的后驱线索
{
pre->rchild = p;
pre->rtag = 1;
}
pre = p; //前驱跟上,当前指针向前遍历
inThreaded(p->rchild,pre); //递归,右子树线索化
}
}
void createThreadTree_in(ThreadNode* root)
{ //利用中序遍历对二叉树进行中序线索化的主过程
ThreadNode *pre = NULL; //前驱结点指针
if(root != NULL) //非空二叉树,线索化
{
inThreaded(root,pre); //中序遍历线索化二叉树
pre->lchild = NULL; //后处理中序最后一个结点
pre->rtag = 1;
}
}
void createBinTreeUsePreAndIn(ThreadNode *&root,char pre[],char in[],int a,int b,int c,int d)
{//由二叉树的先序序列和中序序列构建二叉树
if(a <= b)
{
root = new ThreadNode(); //创建根节点
root->data = pre[a];
root->lchild = NULL;
root->rchild = NULL;
root->ltag = 0;
root->rtag = 0;
int i;
for(i = c; i <= d; i++) //中序序列中查找根的位置,此处为i
{
if(in[i] == pre[a])
break;
}
createBinTreeUsePreAndIn(root->lchild,pre,in,a+1,a+i-c,c,i-1); //递归建左子树
createBinTreeUsePreAndIn(root->rchild,pre,in,a+i-c+1,b,i+1,d); //递归建右子树
}
}
void printBTree(ThreadNode* root)
{
if(root!= NULL)
{
printBTree(root->lchild);
cout<<(char)(root->data)<<"\t";
printBTree(root->rchild);
}
}
int main()
{
int k = 0;
char pre[] = "123456789";
char in[]="324165879";
/* 1
/ 2 5
/ 3 4 6 7
/ 8 9
*/
ThreadNode *root;
createBinTreeUsePreAndIn(root,pre,in,0,8,0,8);
printBTree(root);
cout<<endl;
createThreadTree_in(root);
inOrder(root);
return 0;
}
百度百科: