对于二叉树的遍历,我们可以进行前序遍历、中序遍历以及后序遍历。在这些遍历中,通过任何一个结点我们可以获得其后面结点的信息,但是前面结点的信息全无从得知。为了解决这个问题,产生了线索二叉树。
线索二叉树的定义
假设用二叉链来存储二叉树,链表中包含两个指针:左指针与右指针。对于n个结点的二叉树,用二叉链存储会产生 n+1 个空链域,利用这些空链域存放在某种遍历次序下该节点的前驱结点和后继结点的指针,这些指针称为线索。加上线索的二叉树称为线索二叉树。
概念很生硬,用图来解释一下,假设有一棵二叉树如下左图;用二叉链来存储的这棵树,会产生6个空链域,如下右图:
对这棵树,中序遍历的次序是:42513,因为会产生6个空链域,利用这6个空链域来存储中序遍历的结果,指向前面结点的用蓝线,指向后面结点的用红线,这样就变成了一颗中序线索二叉树。
线索二叉树会针对不同的遍历方式,存在不同的结点指向,而且除了相应序列的第一个结点和最后一个节点,每个节点都有一个前驱和后驱结点。
为什么说“对于n个结点的二叉链,在二叉链存储结构中有n+1个空链域?”
在二叉链中,一个结点有两个指针域,所以n个结点共有2n个链域。又因为在二叉树中,除根结点外,每一个结点有且仅有一个双亲,所以只有n-1个结点的链域存放指向非空子结点的指针,这样就还有n+1个空指针。
了解后线索二叉树后又出现了一个新问题,你怎么知道你的结点指针指向的是它的左右子结点还是它的遍历前后节点呢?这里就要说到线索二叉树的存储结构了:如下图,可以将二叉链的两个指针再分:分成一个指针和一个区域,这个区域用来存储0/1,若为0则代表此刻指针指向的是左右孩子,若为1则代表此刻指针指向的是线索。
再举个例子来理解线索二叉树:对下面这个二叉树将其线索化,线索化前得先遍历二叉树,在遍历过程中,检查当前节点的左、右指针域是否为空,左指针为空就将它改为指向前驱结点,右指针为空就改为指向后驱结点。假设我们现在准备二叉树进行中序遍历线索化,在没有进行线索化之前,这棵树的结构如下图:
接下来,我们对这棵树进行中序线索化,为了方便算法实现,为线索二叉树增加了一个头结点,其中头结点的lchild指向根结点,rchild域指向中序遍历序列中的最后一个结点,该结点的rchild域指向头结点。
二叉树的中序线索化的代码实现(java)
因为是中序线索化,所以要先处理左节点,再处理自己,后处理右节点。因为涉及遍历,所以要递归。首先对二叉树的结构进行处理,添加两个数字:
public class TreeNode {
public int value;//结点的权
public TreeNode left;
public TreeNode right;
//标识指针类型:为0指的是左右子节点,1指的是线索
int leftType = 0;
int rightType = 0;
//......
}
中序线索化的思路为,假设正在操作的结点为 p 且p不为空。p的上一个结点记为pre。因为是中序遍历,所以
- 左子树线索化
- 对本节点线索化:
- 先判断自己的左指针是否为空,为空就指向前驱结点
- 判断前驱结点的右指针是否为空,为空就将前驱结点的右指针指向自己
- 右子树线索化
因为在中序遍历中,对于任意一个节点,我们是不知道它的上一个节点信息的,但是在线索化处理时,一定会用到上一个节点,所以需要临时存储前一个结点。
public class BinaryTree {
TreeNode root;
//用于临时存储前驱节点,为什么要设置这一个?因为在遍历节点时,是获取不到前一个结点的,但是线索化需要指向前一个结点,所以先临时存储
TreeNode pre = null;
//中序线索化二叉树:左-》自己-》右
public void threadNodes(){
threadNodes(root);
}
public void threadNodes(TreeNode node){
if(node == null){
return;
}
//先处理左子树
threadNodes(node.left);
//处理自己
//1. 先判断自己的左指针是否为空,为空就指向前驱结点
if(node.left == null){
node.left = pre;//当前结点的左指针 = 当前结点前驱结点
//改变类型为1
node.leftType = 1;
}
//2. 判断前驱结点的右指针是否为空,为空就将前驱结点的右指针指向自己
if(pre != null && pre.right == null){
pre.right = node;
pre.rightType = 1;
}
//处理完这个节点,这个节点就是下一个节点的前驱结点
pre = node;
//处理右子树
threadNodes(node.right);
}
//......
}
二叉树的中序线索化的代码实现(C)
中序线索化的思路为,先创建一个头结点* head,在进行中序遍历过程中须保留当前节点*p的前驱结点的指针,设为pre(全局变量,初值时指向头结点)。在p不为空的情况下:
- 左子树线索化
- 对本节点线索化:
- 先判断自己的左指针是否为空,为空就指向前驱结点
- 判断前驱结点的右指针是否为空,为空就将前驱结点的右指针指向自己
- 右子树线索化
//数据结构
typedef struct bthnode{
DataType data;
struct bthnode *lchild, rchild;
int ltag, rtag;
}Bthnode;
//对根结点为*p的二叉树进行中序线索化
BthNode *pre;
void Thread(BthNode *&p)
{
if (NULL != p)
{
Thread(p->lchild);
if (NULL == p->lchild)//前驱线索
{
p->lchild = pre;
p->ltag = 1;
}
else p->ltag = 0;
if (NULL == pre->rchild)
{
pre->rchild = p;
pre->rtag = 1;
}
else pre->rtag = 0;
pre = p;
Thread(p->rchild);//右子树线索化
}
}
//对以*bt为根节点的二叉树中序线索化,并增加一个头结点head
BthNode *CreaThread(BthNode *bt)
{
BthNode *head;
head = (BthNode*)malloc(sizeof(BthNode));
head->ltag = 0;
head->rtag = 1;
head->rchild = bt;
if (NULL == bt)
{
head->lchild = head;
}
else
{
head->lchild = bt;
pre = head; //pre是*p的前驱结点
Thread(bt); //中序遍历线索化二叉树
pre->rchild = head; //最后处理,加入指向根结点的线索
pre->rtag = 1;
head->rchild = pre; //根结点右线索化
}
return head;
}
线索二叉树的遍历
因为线索二叉树特殊的结构,在遍历时不需要递归,节省了栈空间。
//遍历线索二叉树
public void Iterate(){
//用于临时存储当前遍历节点
TreeNode node = root;
while(node != null){
//循环找到最开始的节点
while(node.leftType == 0){
node = node.left;
}
//打印当前结点的值
System.out.println(node.value);
//如果当前结点的右指针指向的是后继结点,可能后继结点还有后继结点
while (node.rightType == 1){
node = node.right;
System.out.println(node.value);
}
//替换遍历的结点
node = node.right;
}
}
写一个今天发生的小故事:我今天特别困,午睡的时候睡了很久,过了一会听见我妈喊我别睡了,说是我妹马上要回来了,让我起床给我妹洗个葡萄,然后我就开始在脑海里做斗争,一半我觉得自己该起床了洗葡萄了;一半我又真的很困,困到完全醒不来。好几次斗争的结果都是我又睡过去;但即使我睡过去了,我又会突然意识到我应该起床洗葡萄,然后一轮新的斗争就会再次上演…最终我终于睁开了双眼,但那刻的感觉还是困的,困到眨眼的瞬间都能睡过去,于是我拿起了手机打算刷会手机清醒清醒。我最终赶走了困意准备起床去洗葡萄,我坐在床边看着外面突然意识到我家没有葡萄,而且我妈还没睡醒呢。
所以我是做梦梦到妈妈喊我给妹妹洗葡萄,然后靠着这个梦境里妈妈喊我的画面真实地逼自己醒过来了吗?
我真棒。