已知各种形态的二叉树,假设有n个节点,那么其二叉链表有2n个链域,其中n-1个是非空,n+1个是空链域,那么为了充分利用这些空指针,让这些空指针指向其他节点,这些指针称为线索,该二叉链表又称为线索链表,该二叉树称为线索二叉树(另外设置一个头节点指向树的根)。
建立线索有相应的规则,以中序线索二叉树为例:
对于一个节点,如果它的左孩子为空,则令其左指针指向该节点在中序遍历时的前驱节点;如果它的右孩子为空,则令其指向该节点在中序遍历时的后继结点。(那么很明显为了区分该节点指针指向的是孩子节点还是前驱后继节点,需要设置一个标志位)
对一个二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化。线索化的实质就是将二叉树中的空指针改为指向相应的前驱和后继,而前驱和后继的信息只有在遍历的情况下才能得到。
二叉线索树的存储结构的定义:
typedef enum{
link,thread //link是有子树,标记为0;thread是没有子树,标记为1
}Tag;
typedef struct bnode{
char data;
struct bnode *left,*right;
Tag ltag,rtag; //设置两个左右标记位置
}bnode,*bt;
中序线索化就类似于将一棵树线性为一个中序序列,这个序列中除了首尾节点外每个节点有且仅有一个前驱和后继,首个(即树中最左边的孩子)的前驱指针和末尾(即树中最右边的孩子)后继指针都指回头节点。令头节点的左孩为根节点,右孩为中序遍历时访问的最后一个节点,这样设置的好处是:我们既可以从头节点指向的首个节点顺后继遍历,也可以从末尾节点沿前驱向前遍历。
中序遍历线索化函数:
bt pre; //pre很重要,是一个全局变量,始终指向刚刚访问过的节点!
void inThreading(bt root){
if(root){
inThreading(root->left); //中序遍历,不断递归去找左子树将其线索化,递归到最后一层是该树的最左叶子
if(!root->left){ //如果其左子树为空,线索化其指向pre,pre成为它的前驱节点
root->ltag=thread;
root->left=pre;
}
if(!pre->right){ //同时查看pre节点的右子树,如果其为空,那么pre节点的后继就是当前节点root
pre->rtag=thread;
pre->right=root;
}
pre=root; //更新pre,pre始终指向刚刚访问过的节点
inThreading(root->right); //递归右子树进行线索化
}
}
最后是将一个建立好的二叉树和头节点通过以上函数进行线索化得到相应的线索二叉树:
void btree_to_thread(bt &head,bt root){
head=(bt)malloc(sizeof(bnode));
head->right=head;
head->rtag=link;
if(!root){
head->left=head;
head->ltag=link;
}
else{
pre=head; //pre指向刚刚访问的节点,则初始为指向头节点
head->left=root; //中序遍历,头节点先指向根节点
head->ltag=link;
inThreading(root); //从根节点开始将所有点线索化
pre->right=head; //经过全部线索化后pre现在指向最后一个节点(即最右的节点)
pre->rtag=thread; //末尾节点指回头节点
head->right=pre;
}
}
完整程序代码:
#include<stdio.h>
#include<stdlib.h>
typedef enum{link,thread}Tag;
typedef struct bnode{
char data;
struct bnode *left,*right;
Tag ltag;
Tag rtag;
}bnode,*bt;
bt pre=NULL; //pre很重要,是一个全局变量,始终指向刚刚访问过的节点!
//先序创建二叉树
void CreateTree(bt &root)
{
char ch;
scanf("%c", &ch);
if(ch == '#')
{
root = NULL;
}
else
{
root = (bt)malloc(sizeof(bnode));
root->data = ch;
root->ltag=root->rtag=link; //注意先全部预设为link!后面如果需线索化再置为thread !
CreateTree(root->left);
CreateTree(root->right);
}
}
//从根节点开始将二叉树线索化
void inThreading(bt &root){
if(root){
inThreading(root->left); //中序遍历,不断递归去找左子树将其线索化,递归到最后一层是该树的最左叶子
if(!root->left){ //如果其左子树为空,线索化其指向pre,pre成为它的前驱节点
root->ltag=thread;
root->left=pre;
}
if(!pre->right){ //同时查看pre节点的右子树,如果其为空,那么pre节点的后继就是当前节点root
pre->rtag=thread;
pre->right=root;
}
pre=root; //更新pre,pre始终指向刚刚访问过的节点
inThreading(root->right); //递归右子树进行线索化
}
}
//中序遍历线索二叉树
void inorderTraverse(bt head){
bt p=head->left;
while(p!=head){ //空树或者遍历结束时p==head
while(p->ltag==link){ //因为是中序遍历,递归优先找左子树,输出完成对“左”的遍历
p=p->left;
}
printf("%c ",p->data);
while(p->rtag==thread&&p->right!=head){ //如果其没有右子树,就找到它的后继点(也就是从左回到根)并输出完成“根”的遍历
p=p->right; //如果有了右子树就跳出,对这个右子树重新开始大循环
printf("%c ",p->data);
}
p=p->right;
}
}
//传入头节点和树的根节点,得到带头节点的线索二叉树
void btree_to_thread(bt &head,bt root){
head=(bt)malloc(sizeof(bnode));
head->right=head;
head->rtag=link;
if(!root){
head->left=head;
head->ltag=link;
}
else{
pre=head; //pre指向刚刚访问的节点,则初始为指向头节点
head->left=root; //中序遍历,头节点先指向根节点
head->ltag=link;
inThreading(root); //从根节点开始将所有点线索化
pre->right=head; //经过全部线索化后pre现在指向最后一个节点(即最右的节点)
pre->rtag=thread; //末尾节点指回头节点
head->right=pre;
}
}
int main(){
bt root,head;
CreateTree(root);
btree_to_thread(head,root);
inorderTraverse(head);
return 0;
}
线索二叉树的意义:
普通二叉树遍历可以使用栈队列非递归遍历或者直接递归遍历,但是递归遍历会调用系统栈,而非递归遍历需要使用内存空间来帮助遍历。如果将该二叉树一次线索化后,其后所有的遍历都不再需要以上的开销了,就像遍历数组一样方便。