揽
1. 前序
线索二叉树之所以加上线索,是因为能够存储存储前驱结点的指针和后继的指针叫线索过程,加上线索二字的二叉树称之为线索二叉树。
为什么需要线索二叉树?对于一个二叉树而言,每一个存储结构都有左孩子指针域和右孩子指针域,它可以按照一定的次序进行遍历,但每一个根结点只能找到它的左右孩子不能找到上一个结点,如果需要找到上一个结点又只能从上到下顺序遍历,线索二叉树便于查找每一个结点的上一个结点是谁也可以找到下一个结点是谁。这就是普通二叉树与加上线索的二叉树的区别所在。
如何实现线索呢?对于n个结点的二叉树,必定含有n+1个空的指针域,利用空的指针域存储前驱结点或者后继结点,吧空的指针域充分利用起来。对于n个结点,每一个结点包含左右孩子,所以有2n个指针域和n-1个非空的指针域,所以空的指针域 = 总指针域个数 - 非空指针域,所以空的指针域 = 2n - (n-1) = 2n - n +1 = n+1。
2.定义数据结构
lchild:左孩子指针域
LTag:前驱索引标志
data:数据域
RTag:后继索引标志
rchiild:右孩子指针域
LTag = 1的时候,表示左孩子指针域存储的是前驱结点
LTag = 0的时候,表示左孩子指针域存储的是左孩子结点
RTag = 1的时候,表示右孩子指针域存储的是后继结点
RTag = 0的时候,表示右孩子指针域存储的是右孩子结点
3.线索二叉树的规律
对于中序线索而言
后继规律:该结点的后继应是该结点的右子树时访问的第一个结点,或者说,该节点的右子树的最左下的结点
前驱规律:该节点的前驱应是该结点的左子树最后访问的结点,或者说该结点的左子树的最右下的结点
对于后序线索而言
分3种情况
1.若结点x是二叉树的根,则后继是空
2.若结点x是双亲的右孩子或者说是其双亲的左孩子且其双亲没有右子树,则其后继即为双亲结点
3.若结点x是双亲的左孩子且其双亲有右孩子,则其后继为其双亲的右子树按后序遍历的第一个结点
3.1 图示
中序遍历顺序:a+bc-d-e/f
后序遍历顺序:abcd-+ef/-
根据中序索引规律,例如根结点的"-“的前驱是其左子树的最右下角,那就是结点d
根据中序索引规律,例如根结点的”-"的后继是其右子树的最左下角,那就是e
4.手算线索过程
需要一个前驱指针和一个根指针,先判断根指针指向结点里面的的左孩子是否为NULL,如果是NULL,那么将根所指向的结点里面的LTag = 1,左孩子存储的是前驱指针,然后判断前驱指针所指向的结点的右孩子是否为NULL,如果右孩子为NULL,那么将前驱指针里面的RTag = 1,并且右孩子存储的是根指针
中序线索伪算法
设置pre为全局作用域前驱指针
pre = NULL
void InThread(BTree root)
{
如果root不等于NULL
{
1.左子树搜索
2.线索过程
2.1.root没有左孩子
设置前驱索引为1,并且左孩子存储pre
2.1.pre!=NULL && pre没有右孩子
设置后继索引为1,并且右孩子存储root
pre = root用来保存上一次根节点的位置
3.右子树搜索
}
}
中序索引图解
中序遍历:DGBHEACF
1.第一次,root指向的是D,pre = NULL,根没有左孩子,设置左索引LTag = 1,左指针域存储前驱pre
2.第二次,根没有左孩子,设置左索引LTag = 1,左指针域存储前驱pre,pre有右孩子,不管它
3.第三次,根有左孩子,不管,pre没有右孩子,设置RTag = 1, rchild存储root
4.第四次,root没有左孩子,设置LTag = 1,左孩子存储pre, pre有右孩子,不管
5.第五次,root有左孩子,不管,pre没有右孩子,设置RTag = 1,右孩子存储root
6.第六次,root有左孩子,不管,pre没有右孩子,设置RTag = 1,右孩子存储root
7.第七次,root没有左孩子,设置LTag = 1, 左孩子存储pre,pre有右孩子,不管
8.第八次,root没有左孩子,设置LTag = 1, 左孩子存储pre,pre有右孩子,不管
9.第九次,最终如下图所示,pre指向最后一个结点
5.问题以及改进
1.为什么4模块中的图解,root按照这种规律进行遍历呢?
这是因为它是按照中序的方式搜索,所以按照中序方式回退,而pre每次指向的是上一次root的位置
2.pre前驱指针得设置全局作用域,初始化NULL,如果不用全局作用域,那么也可以将pre设置static,静态。
3.改进,根据图解中,发现第一个结点D中的左孩子是NULL,以及最后一个结点的右孩子是NULL,可以在此二叉树加一个头结点,该头节点的左孩子存储的是root,root中序遍历第一个结点左孩子存储的是头结点。
root中序遍历最后一个结点的右孩子存储头结点。头结点的右孩子存储最后一个结点
如图
6.查找前驱和后继算法
查找前驱
叶子结点
判断LTag是否等于1,如果是,那么左孩子存储的是该结点的前驱
非叶子结点
该结点的左孩子,然后一直查找右孩子,当RTag = 1就退出循环,当RTag = 0就继续循环查找右孩子
查找后继
叶子结点
判断RTag是否等于1,如果是,那么右孩子存储的是该结点的后继
非叶子结点
该结点的右孩子,然后一直查找左孩子,当LTag = 1就退出循环,当LTag = 0就继续循环查找左孩子
前驱查找代码
//前驱查找
void InPre(BiTree root, BiTree* pre) {
//叶子结点前驱查找
//只需要判断左索引是否是1,如果是,那么该结点的前驱存储在该结点的左孩子
if (root->ltag == 1)
*pre = root->Lchild;
//非叶子结点前驱查找
//该节点的左孩子的右孩子最后一个结点,最后一个结点肯定是叶子结点,既然是叶子结点,那么ltag,rtag 都是 1
//查找该结点的左孩子的右孩子然后一直查找右孩子最终肯定是一个叶子结点, 如果不是叶子结点rtag = 0
//当查找到某一个结点的结点的rtag = 1,那么必定是叶子结点,就是它的前驱结点
else {
BiTree p;
for (p = root->Lchild; p->rtag == 0; p = p->Rchild);
*pre = p;
}
}
后继查找代码
//后继查找
void InSucc(BiTree root, BiTree* pre) {
//叶子结点后继查找
//右索引是否是1,如果是,那么该结点的右孩子必定是该结点的后继
if (root->rtag == 1)
*pre = root->Rchild;
//非叶子结点后继查找
//该结点的右孩子的左孩子的第一个结点,必定是叶子结点,同时也是该结点的后继
//查找该结点的右孩子然后一直查找左孩子,最终该结点肯定是一个叶子结点,就找到它的后继
else {
BiTree p;
for (p = root->Rchild; p->ltag == 0; p = p->Lchild);
*pre = p;
}
}
7.代码
7.1建立二叉树
//先序建立一棵树
void Create_Tree(BiTree* root) {
char ch;
scanf("%c", &ch);
if (ch == '#') (*root) = NULL;
else {
(*root) = (BiTree)malloc(sizeof(Tree));
(*root)->data = ch;
(*root)->ltag = 0;
(*root)->rtag = 0;
Create_Tree(&((*root)->Lchild));
Create_Tree(&((*root)->Rchild));
}
}
7.2 中序线索
//中序线索
void InThread(BiTree root) {
if (root != NULL) {
//左搜索
InThread(root->Lchild);
//索引
//根的左孩子索引判断
if (root->Lchild == NULL) {
root->ltag = 1;
root->Lchild = pre;
}
//前驱的右孩子索引判断
if (pre && pre->Rchild == NULL) {
pre->rtag = 1;
pre->Rchild = root;
}
//更新前驱结点
pre = root;
//右搜索
InThread(root->Rchild);
}
}
7.4 前驱查找
//前驱查找
void InPre(BiTree root, BiTree* pre) {
//叶子结点前驱查找
//只需要判断左索引是否是1,如果是,那么该结点的前驱存储在该结点的左孩子
if (root->ltag == 1)
*pre = root->Lchild;
//非叶子结点前驱查找
//该节点的左孩子的右孩子最后一个结点,最后一个结点肯定是叶子结点,既然是叶子结点,那么ltag,rtag 都是 1
//查找该结点的左孩子的右孩子然后一直查找右孩子最终肯定是一个叶子结点, 如果不是叶子结点rtag = 0
//当查找到某一个结点的结点的rtag = 1,那么必定是叶子结点,就是它的前驱结点
else {
BiTree p;
for (p = root->Lchild; p->rtag == 0; p = p->Rchild);
*pre = p;
}
}
7.5 后继查找
//后继查找
void InSucc(BiTree root, BiTree* pre) {
//叶子结点后继查找
//右索引是否是1,如果是,那么该结点的右孩子必定是该结点的后继
if (root->rtag == 1)
*pre = root->Rchild;
//非叶子结点后继查找
//该结点的右孩子的左孩子的第一个结点,必定是叶子结点,同时也是该结点的后继
//查找该结点的右孩子然后一直查找左孩子,最终该结点肯定是一个叶子结点,就找到它的后继
else {
BiTree p;
for (p = root->Rchild; p->ltag == 0; p = p->Lchild);
*pre = p;
}
}
7.6 加入头节点线索二叉树
//加入头结点中序线索二叉树
void InOrderThreading(BiTree& Thrt, BiTree root) {
if (!(Thrt = (BiTree)malloc(sizeof(Tree)))) exit(OVERFLOW);
Thrt->ltag = 0; Thrt->rtag = 1;
Thrt->Rchild = Thrt; //回指自己
if (!Thrt) Thrt->Lchild = Thrt; //二叉树不存在
else {
Thrt->Lchild = root; pre = Thrt; //前驱指针指向头结点
InThread(root); //中序线索
pre->Rchild = Thrt; pre->rtag = 1; //最后一个结点线索
Thrt->Rchild = pre;
}
}
7.7 非递归中序遍历线索二叉树
//非递归遍历线索二叉树
void InOrderTraverse_Thr(BiTree thrt) {
BiTree p = thrt->Lchild; //找到根结点
while (p != thrt) {
while (p->ltag == 0) p = p->Lchild;
printf("%c ", p->data);
while (p->rtag == 1 && p->Rchild != thrt) {
p = p->Rchild; printf("%c ", p->data);
}
p = p->Rchild;
}
}
8.测试案例
Tree.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#define OVERFLOW -1
typedef char ElemType;
typedef int PointerType;
typedef struct BTree {
ElemType data;
struct BTree* Lchild, * Rchild; //指向左右孩子指针
PointerType ltag, rtag; //左右索引标志 为1的时候进行索引,为0指向孩子
}Tree, *BiTree;
extern BiTree pre;
//先序建立一棵树
void Create_Tree(BiTree* root);
//中序线索
void InThread(BiTree root);
//前驱查找
void InPre(BiTree root, BiTree* pre);
//后继查找
void InSucc(BiTree root, BiTree* pre);
//加入头结点中序线索二叉树
void InOrderThreading(BiTree& Thrt, BiTree root);
//非递归遍历线索二叉树
void InOrderTraverse_Thr(BiTree thrt);
TreeOperation.cpp
#include "Tree.h"
//先序建立一棵树
void Create_Tree(BiTree* root) {
char ch;
scanf("%c", &ch);
if (ch == '#') (*root) = NULL;
else {
(*root) = (BiTree)malloc(sizeof(Tree));
(*root)->data = ch;
(*root)->ltag = 0;
(*root)->rtag = 0;
Create_Tree(&((*root)->Lchild));
Create_Tree(&((*root)->Rchild));
}
}
//中序线索
void InThread(BiTree root) {
if (root != NULL) {
//左搜索
InThread(root->Lchild);
//索引
//根的左孩子索引判断
if (root->Lchild == NULL) {
root->ltag = 1;
root->Lchild = pre;
}
//前驱的右孩子索引判断
if (pre && pre->Rchild == NULL) {
pre->rtag = 1;
pre->Rchild = root;
}
//更新前驱结点
pre = root;
//右搜索
InThread(root->Rchild);
}
}
//前驱查找
void InPre(BiTree root, BiTree* pre) {
//叶子结点前驱查找
//只需要判断左索引是否是1,如果是,那么该结点的前驱存储在该结点的左孩子
if (root->ltag == 1)
*pre = root->Lchild;
//非叶子结点前驱查找
//该节点的左孩子的右孩子最后一个结点,最后一个结点肯定是叶子结点,既然是叶子结点,那么ltag,rtag 都是 1
//查找该结点的左孩子的右孩子然后一直查找右孩子最终肯定是一个叶子结点, 如果不是叶子结点rtag = 0
//当查找到某一个结点的结点的rtag = 1,那么必定是叶子结点,就是它的前驱结点
else {
BiTree p;
for (p = root->Lchild; p->rtag == 0; p = p->Rchild);
*pre = p;
}
}
//后继查找
void InSucc(BiTree root, BiTree* pre) {
//叶子结点后继查找
//右索引是否是1,如果是,那么该结点的右孩子必定是该结点的后继
if (root->rtag == 1)
*pre = root->Rchild;
//非叶子结点后继查找
//该结点的右孩子的左孩子的第一个结点,必定是叶子结点,同时也是该结点的后继
//查找该结点的右孩子然后一直查找左孩子,最终该结点肯定是一个叶子结点,就找到它的后继
else {
BiTree p;
for (p = root->Rchild; p->ltag == 0; p = p->Lchild);
*pre = p;
}
}
//加入头结点中序线索二叉树
void InOrderThreading(BiTree& Thrt, BiTree root) {
if (!(Thrt = (BiTree)malloc(sizeof(Tree)))) exit(OVERFLOW);
Thrt->ltag = 0; Thrt->rtag = 1;
Thrt->Rchild = Thrt; //回指自己
if (!Thrt) Thrt->Lchild = Thrt; //二叉树不存在
else {
Thrt->Lchild = root; pre = Thrt; //前驱指针指向头结点
InThread(root); //中序线索
pre->Rchild = Thrt; pre->rtag = 1; //最后一个结点线索
Thrt->Rchild = pre;
}
}
//非递归遍历线索二叉树
void InOrderTraverse_Thr(BiTree thrt) {
BiTree p = thrt->Lchild; //找到根结点
while (p != thrt) {
while (p->ltag == 0) p = p->Lchild;
printf("%c ", p->data);
while (p->rtag == 1 && p->Rchild != thrt) {
p = p->Rchild; printf("%c ", p->data);
}
p = p->Rchild;
}
}
main.cpp
#include "Tree.h"
BiTree pre = NULL;
int main() {
BiTree root;
BiTree Thrt = NULL; //头结点
//先序建立一颗二叉树
Create_Tree(&root);
getchar();
InOrderThreading(Thrt, root); //中序带头结点线索
InOrderTraverse_Thr(Thrt); //非递归遍历线索二叉树
getchar();
getchar();
return 0;
}
/*
ABD#G##EH###C#F##
*/
9.运行结果
10.增加一个功能
利用中序遍历线索二叉树非递归算法,查找并且输出每一个结点的前驱和后继结点,直接在非递归线索二叉树输出的printf那里改一下,将每次的结点调用查找前驱结点和后继结点的接口,就能实现一个输出线索二叉树中的每一个结点的前驱结点和后继结点的信息。