遍历二叉树和线索二叉树
遍历二叉树
-
遍历定义
顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。
-
遍历目的——得到树中所有结点的一个线性排列。
-
遍历用途——它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
-
遍历方法——依次遍历二叉树中的三个组成部分,便是遍历了整个二叉树
假设:L:遍历左子树,D:访问根结点,R:遍历右子树
则遍历整个二叉树方案共有:DLR、LDR、LRD、DRL、RDL、RLD六种。- 规定先左后右,则有:DLR-先序遍历(根左右),LDR-中序遍历(左右根),LRD-后序遍历(左右根)
- 规定先左后右,则有:DLR-先序遍历(根左右),LDR-中序遍历(左右根),LRD-后序遍历(左右根)
-
遍历二叉树算法描述
先序遍历 | 中序遍历 | 后序遍历 |
---|---|---|
若二叉树为空,则空操作; 否则 (1)访问根结点; (2)先序遍历左子树; (3)先序遍历右子树 | 若二叉树为空,则空操作; 否则 (1)中序遍历左子树; (2)访问根结点; (3)中序遍历右子树 | 若二叉树为空,则空操作; 否则 (1)后序遍历左子树; (2)后序遍历右子树; (3)访问根结点 |
例题:
-
根据遍历序列确定二叉树
由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以确定唯一一棵二叉树 -
先序遍历算法实现
根左右
//二叉链表的递归先序遍历
Status PerOrderTraverse(BiTree T){
if(T==NULL)return OK;//空二叉树
else{
vist(T);//访问根结点,eg:输出:printf("%d ",T->data);
PerOrderTraverse(T->lchild);//递归遍历左孩子
PerOrderTraverse(T->rchild);//递归遍历右孩子
}
}
- 中序遍历算法实现
左根右
//二叉链表的递归中序遍历
Status InOrderTraverse(BiTree T){
if(T==NULL)return OK;//空二叉树
else {
InOrderTraverse(T->lchild);//递归遍历左孩子
visit(T);//访问根结点
InOrderTraverse(T->rchild);//递归遍历右子树
}
}
- 后序遍历算法实现
左右根
//二叉链表的递归后序遍历
Status PostOrderTraverse(BiTree T){
if(T==NULL)return OK;
else {
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
visit(T);
}
}
三种遍历的时间复杂度: O ( n ) O(n) O(n),空间复杂度: O ( n ) O(n) O(n)
- 中序遍历的非递归算法
二叉树中序遍历的非递归算法的关键:在中序遍历过某结点的整个左子树后,如何找到该结点的根以及右子树。
基本思想:
(1)建立一个栈
(2)根结点进栈,遍历左子树
(3)根结点出栈,输出根结点,遍历右子树。
//二叉树的非递归算法--栈
Status InOrderTraverse(BiTree T){
BiTree p; //初始变量p
InitStack(S); //初始化栈
p=T; //指向根结点
while(p||!StackEmpty(S)){//p不为空,栈不为空
if(p){//变量p不为空
Push(S,p); //压入栈
p=p->lchild; //遍历左孩子
}
else { //变量p为空
Pop(S,q); //弹出栈顶元素
printf("%c",q->data); //输出栈顶元素值
p=q->rchild; //访问栈顶的右孩子
}
}
return OK;
}
- 二叉树的层次遍历
算法设计思路:使用一个队列- 1.将根结点进队;
- 2.队不空时循环:从队列中出列一个结点*p,访问它;
- 1.若它有左孩子结点,将左孩子结点进队;
- 2.若它有右孩子结点,将右孩子结点进队。
#define MAXSIZE 100
//队列的类型定义
typedef struct{
BTNode data[MAXSIZE];//存放元素
int front,rear;//头尾指针
}SqQueue;
//二叉树的层次遍历
void LeveOrder(BTNode *b){
BTNode *p;
SqQueue *qu;
InitQueue(qu);//初始化队列
enQueue(qu,b);//根结点指针进入队列
while(!QueueEmpty(qu)){//队列不为空,则循环
deQueue(qu,p);//出队结点p
printf("%c ",p->data);//访问节点p
if(p->lchild!=NULL) enQueue(qu,p->lchild);
//有左孩子时将其入队
if(p->rchild!=NULL) enQueue(qu,p->rchild);
//有右孩子时将其入队
}
}
- 二叉树的建立
根据先序遍历:
ABC##DE#G##F###
Status CreateBiTree(BiTree &T){
scanf(&ch);//cin>>ch;
if(ch=="#") T=NULL;
else {
if(!(T=(BiTNode *)malloc(sizeof(BiTNode))))//
exit(OVERFAOW);// T=new BiNode;
T->data=ch;//生成根结点
CreateBiTree(T->lchild);//构造左子树
CreateBiTree(T->rchild);//构造右子树
}
return OK;
}//CreateBiTree
示意图:
遍历二叉树的应用
复制二叉树
- 如果是空树,递归结束
- 否则,申请新结点空间,复制根结点
- 递归复制左子树
- 递归复制右子树
int Copy(BiTree T,BiTree &NewT){
if(T==NULL) NewT=NULL,return 0;//空则返回0
else {
NewT=new BiTNode; //分配空间
NewT->data=T->data; //复制根结点
Copy(T->lchild,NewT->lchild); //递归复制左子树
Copy(T->rchild,NewT->rchild); //递归复制右子树
}
}
计算二叉树的深度
- 如果是空树,则深度为0
- 否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1。
int Depth(BiTree T){
if(T==NULL) return 0; //树空返回0
else {
m=Depth(T->lchild); //递归计算左子树的深度
n=Depth(T->rchild); //递归计算右子树的深度
if(m>n)return m+1;
else return n+1;
}
}
计算二叉树的结点总数
- 如果是空树,则结点个数为0
- 否则,结点个数为左子树的结点个数 + + +右子树的结点个数再 + 1 +1 +1。
int NodeCount(BiTree T){
if(T==NULL)return 0;
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
计算二叉树叶子结点数
- 如果是空树,则叶子结点个数为0。
- 否则,为左子树的叶子结点个数 + + +右子树的叶子结点个数。
int LeafCount(BiTree T){
if(T==NULL)return 0;//树空返回0
if(T->lchild==NULL&&T->rchild==NULL) return 1; //是叶子结点返回1
else return LeafCount(T->lchild)+LeafCount(T->rchild); //递归遍历左右子树
}
线索二叉树
-
为什么要引入线索二叉树
当用二叉链表作为二叉树的存储结构时,可以很方便地找到某个结点的左右孩子;但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点。 -
解决的方法
1、通过遍历寻找–费时间
2、再增设前驱、后继指针域–增加了存储负担。
3、利用二叉链表中的空指针域 -
利用二叉链表中的空指针域–线索二叉树的定义
如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继
----这种改变指向的指针称为==“线索”==
加上了线索的二叉树称为线索二叉树(Threaded Binary Tree))
对叉树按某种遍历次序使其变为线索二叉树的过程叫线索化 -
线索化示意图
- 区分指针域
为区分lrchid和rchild指针到底是指向孩子的指针,还是指向前驱或者后继的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定:- tag=0 lchild指向该结点的左孩子
- tag=1 lchild指向该结点的前驱
- tag=0 rchild指向该结点的右孩子
- tag=1 rchild指向该结点的后继
结点结构示意图:
typedef struct BiThrNode{
int data;
int ltag,rtag;
struct BiThrNode *lchild,*rchild;
}BiThrNode,*BiThrTree;
各种线索二叉树的示意图: