栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【六、树与二叉树】6.3二叉树的遍历、线索二叉树

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【六、树与二叉树】6.3二叉树的遍历、线索二叉树

目录

一、二叉树的遍历

1.遍历

2.手工遍历二叉树

3.先(前)序遍历

(1)递归实现

(2)非递归实现

4.中序遍历

(1)递归实现

(2)非递归实现

5.后序遍历

(1)递归实现

(2)非递归实现

6.层序遍历

7.由遍历的序列确定二叉树

(1)如何确定一棵二叉树

(2)先序序列、中序序列可以唯一地确定一棵二叉树

(3)后序序列、中序序列可以唯一地确定一棵二叉树

(4)层序序列、中序序列可以唯一地确定一棵二叉树

(5)注解

二、线索二叉树

1.前言

2.线索二叉树的基本概念

3.手工画线索二叉树

4.中序线索二叉树

(1)中序线索二叉树的构造

(2)中序线索二叉树的遍历

5.前序线索二叉树

6.后序线索二叉树


一、二叉树的遍历

1.遍历

二叉树的遍历:使二叉树中的结点被访问且只被访问一次

遍历规则(设根为P,左子树为L,右子树为R):

先序遍历

PLR
中序遍历LPR
后序遍历LRP

前、中、后指的都是根被访问的次序;左右的相对顺序都一样;

强调:树是一个递归定义

2.手工遍历二叉树

步骤:

①根据遍历规则在所有结点上的相应位置上进行标记

先序遍历标记左侧
中序遍历标记下侧
后序遍历标记右侧

②按从上至下,从左至右的顺序,给树描边

③顺着描边的路径,与①中标记相遇的顺序就是对应遍历方法的顺序

例子:

3.先(前)序遍历

(1)递归实现

思想:

二叉树为空结束
二叉树不为空①访问根结点②先序遍历左子树③先序遍历右子树

例子:

代码:

void visit(BiTNode* node) {
	cout << node->data << endl;
}

void preOrder(BiTree T) {
	if (T != NULL) {     //空树什么也不做,非空树才进行操作
		visit(T);        //先访问根结点
		preOrder(T->lchild);   //再先序遍历左子树
		preOrder(T->rchild);   //再先序遍历右子树
	}
}

(2)非递归实现

前言:

①树是一个递归的定义

②我们只需要处理好:根、左子树、右子树的访问关系就行

③左子树、右子树本身也是树,它已经被我安排的明明白白了

④核心:PLR

思想:

1)初始化:根结点入栈
2)后续:重复,直到栈空2.1)栈顶出栈,访问根
2.2)根右子树(孩子)入栈
2.3)根左子树(孩子)入栈

为什么左子树后入栈?因为位于栈顶的元素最先被访问。

对于子树,就是一棵树,按处理树的完整流程去做就行。

例子:

代码:

void preOrderNoRecursion(BiTree bt) {
	BiTNode* Stack[MaxSize];   //定义栈
	int top = -1;              //定义栈顶指针,并初始化为-1

	BiTNode* p;                //临时指针

	if (bt != NULL) {          //不是空树,则进行遍历
		// 1)初始化,根结点入栈
		Stack[++top] = bt;

		// 2)循环执行,直至栈空
		while (top != -1) {
			// 2.1)取栈顶结点,访问之
			p = Stack[top--];
			visit(p);
			// 2.2)2.3)将该结点的右孩子,左孩子依次入栈
			if (p->rchild != NULL) {
				Stack[++top] = p->rchild;
			}
			if (p->lchild != NULL) {
				Stack[++top] = p->lchild;
			}
		}
	}
}

4.中序遍历 (1)递归实现

思想:

二叉树为空结束
二叉树不为空①中序遍历左子树②访问根结点③中序遍历右子树

例子:

代码:

void visit(BiTNode* node) {
	cout << node->data << endl;
}

void inOrder(BiTree T) {
	if (T != NULL) {     //空树什么也不做,非空树才进行操作
		inOrder(T->lchild);   //中序遍历左子树
		visit(T);             //访问根结点
		inOrder(T->rchild);   //中序遍历右子树
	}
}
(2)非递归实现

前言:

①树是一个递归的定义

②我们只需要处理好:根、左子树、右子树的访问关系就行

③左子树、右子树本身也是树,它已经被我安排的明明白白了

④核心:LPR

思想:

1)初始化:根结点入栈
2)观测栈顶结点(不出栈)栈顶结点有左孩子,左孩子入栈——重复2)
栈顶结点无左孩子,该结点出栈并访问之——执行2.1)2.1)观测该结点右孩子有右孩子,右孩子入栈——重复2)
无右孩子栈空——结束
栈非空——栈顶元素出栈,访问之,重复2.1)

对于子树,就是一棵树,按处理树的完整流程去做就行。

2.1)思想设计原因见下面表格

没有右子树,意味着当前这棵树已经遍历完毕
当前这棵树是上面树的左子树——下面应该访问上面树的根
当前这棵树是上面树的右子树——上面整个树已经访问完毕了,对上面这棵树同理,最终走到整体是某棵树的左子树,或者栈空

例子:

代码:

void inOrderNoRecursion(BiTree bt) {
	BiTNode* Stack[MaxSize];  //定义栈
	int top = -1;             //定义栈顶指针,并初始化为-1
	BiTNode* p;               //临时指针
	bool rightEmpty = false;

	if (bt != NULL) {       //不是空树,则进行遍历
		//1)初始化,根入栈
		Stack[++top] = bt;

		//2)循环执行,直至栈空
		while (top != -1) {
			//读取栈顶结点,不出栈
			p = Stack[top];

			if (p->lchild == NULL || rightEmpty) {
				rightEmpty = false;

				//出栈栈顶元素,并访问
				top--;
				visit(p);
				//该结点有右孩子的话,右孩子入栈
				if (p->rchild != NULL) {
					Stack[++top] = p->rchild;
				}
				else {//该结点没有右孩子
					rightEmpty = true;
				}
			}
			else {
				Stack[++top] = p->lchild;
			}
		}
	}
}

5.后序遍历 (1)递归实现

思想:

二叉树为空结束
二叉树不为空①后序遍历左子树②后序遍历右子树③访问根结点

例子:

代码:

void visit(BiTNode* node) {
	cout << node->data << endl;
}

void postOrder(BiTree T) {
	if (T != NULL) {     //空树什么也不做,非空树才进行操作
		postOrder(T->lchild);    //后序遍历左子树
		postOrder(T->rchild);    //后序遍历右子树
		visit(T);                //访问根结点
	}
}
(2)非递归实现

前言:

①树是一个递归的定义

②我们只需要处理好:根、左子树、右子树的访问关系就行

③左子树、右子树本身也是树,它已经被我安排的明明白白了

④核心:LRP

思想:

1)初始化:根结点入栈
2)观测栈顶结点(不出栈)2.1)栈顶结点有左孩子,左孩子入栈——重复2)
2.2)栈顶结点有右孩子且右孩子未被访问,右孩子入栈——重复2)
2.3)栈顶结点无右孩子,该结点出栈并访问之——栈不为空,重复2)【不含2.1)】,栈为空,结束
2.4)栈顶结点有右孩子且右孩子已被访问——该结点出栈并访问之——栈不为空,重复2)【不含2.1)】,栈为空,结束

对于子树,就是一棵树,按处理树的完整流程去做就行。

注解结论
当左子树访问完毕后,会回到根结点

栈顶结有右子树且右子树未被访问——右孩子入栈

栈顶结有右子树且右子树已被访问——访问根结点

当右子树访问完毕后,会回到根结点
右子树的访问次序在根结点的前一位(写代码时,用一个指针变量指向前一个被访问的结点就可以)
2.2)
2.3)
2.4)

例子:

代码:

void postOrderNoRecursion(BiTree bt) {
	BiTNode* Stack[MaxSize];   //定义栈
	int top = -1;     //定义栈顶指针,并初始化为-1
	BiTNode* pre = NULL;  //当前访问结点之前被访问的结点
	BiTNode* p;           //临时指针
	bool visited = false;

	if (bt != NULL) {     //不是空树,则进行遍历
		//1)初始化,根入栈
		Stack[++top] = bt;

		//2)循环执行,直至栈空
		while (top != -1) {
			//读取栈顶结点,不出栈
			p = Stack[top];

			if (p->lchild == NULL || visited) {
				visited = false;
				//栈顶结点有右子树 且 右子树未被访问:右子树入栈
				if (p->rchild != NULL && p->rchild != pre) {
					Stack[++top] = p->rchild;
			    //栈顶结点无右子树、栈顶结点有右子树 且 右子树已被访问:
				//该结点出栈并访问之
				}
				else if (p->rchild == NULL
					|| (p->rchild != NULL && p->rchild == pre)) {
					//出栈栈顶元素,并访问
					top--;
					visit(p);
					//更新pre
					pre = p;
					visited = true;
				}
			}
			else { //有左孩子,左孩子入栈
				Stack[++top] = p->lchild;
			}
		}
	}
}

6.层序遍历

前言:从左至右、从上至下、按行遍历

例子:

遍历结果为ABCDEFGHI

思想:

1)初始化:根结点入队
2)后续:重复,直到栈空队空:遍历完成
队非空:出队,访问出队的结点该结点有左孩子:左孩子入队
该结点有右孩子:右孩子入队
重复2)

例子:

void levelOrder(BiTree T) {
	//初始化队列
	Queue Q;
	InitQueue(Q);

	//工作指针
	BiTNode* p;

	//初始化根结点,入队
	EnQueue(Q, T);

	//队列非空,循环
	while (!isEmpty(Q)) {
		//出队元素,交给p
		DeQueue(Q, p);
		//执行访问
		visit(p);
		//有左孩子
		if (p->lchild != NULL) {
			EnQueue(Q, p->lchild);
		}
		//有右孩子
		if (p->rchild != NULL) {
			EnQueue(Q, p->rchild);
		}
	}
}

7.由遍历的序列确定二叉树

(1)如何确定一棵二叉树

例子:

前序遍历P={A},L={B,D,F},R={C,E,G,H}

前序遍历:根、左子树、右子树

中序遍历:左子树、根、右子树

后序遍历:左子树、右子树、根

不考虑左右子树内部顺序,则它们三者一定是独立出现的

中序遍历L={B,D,F},P={A},R={C,E,G,H}
后序遍历L={B,D,F},R={C,E,G,H},P={A}
层序遍历A,B,C,D,E,F,G,H(子树E层序遍历为E,G,H)对子树来说,层序遍历的结果与整个树的层序遍历的结果相对一致

如何确定一棵二叉树?

①能找到根

②能划分出左右子树(对左右子树来说是同样的问题,所以得解)

(2)先序序列、中序序列可以唯一地确定一棵二叉树

思想:

在先序序列中第一个结点一定是二叉树的根结点
在中序序列中根结点将中序序列分割成两个子序列左侧是根结点的左子树的中序序列(重复这个过程)
右侧是根结点的右子树的中序序列(重复这个过程)

例子:

已知前序序列为ABCDEFGHI;中序序列为BCAEDGHFI,确定二叉树

步骤处理过程当前处理结果
1)

①当前处理子树

前序:ABCDEFGHI

中序:BCAEDGHFI

②得出结果

根:A【由前序得出】

左子树:集合BC【由中序得出】

右子树:集合EDGHFI【由中序得出】

2)

①当前处理子树

前序:BC

中序:BC

②得出结果

根:B【由前序得出】

左子树:空【由中序得出】

右子树:C【由中序得出】

3)

①当前处理子树

前序:DEFGHI

中序:EDGHFI

②得出结果

根:D【由前序得出】

左子树:E【由中序得出】

右子树:GHFI【由中序得出】

4)

①当前处理子树

前序:FGHI

中序:GHFI

②得出结果

根:F【由前序得出】

左子树:GH【由中序得出】

右子树:I【由中序得出】

5)

①当前处理子树

前序:GH

中序:GH

②得出结果

根:G【由前序得出】

左子树:空【由中序得出】

右子树:H【由中序得出】

(3)后序序列、中序序列可以唯一地确定一棵二叉树

思想:

在后序序列中最后一个结点一定是二叉树的根结点
在中序序列中根结点将中序序列分割成两个子序列左侧是根结点的左子树的中序序列(重复这个过程)
右侧是根结点的右子树的中序序列(重复这个过程)

(4)层序序列、中序序列可以唯一地确定一棵二叉树

思想:

1.层次遍历确定根

2.中序遍历确定左右子树

3.左右子树的层次遍历结果与原层次遍历的相对顺序一致

(5)注解

①前、后、层+中可以唯一确定一棵二叉树,其它组合均不行

②对于二叉排序树,二叉排序树默认知道中序序列(递增),所以只需要知道前、后、层就能还原出来二叉排序树【已知二叉排序树的前序序列为13452,则默认知道中序序列为12345】

二、线索二叉树

1.前言

使用二叉链表存储二叉树,存在n+1个空指针域

使用具体遍历方法,得到的遍历结果是一个线性结果

可以利用空指针域去存储某个结点在某种遍历方式下的直接前驱或直接后继

2.线索二叉树的基本概念

说明:

①二叉树遍历的实质——将非线性结构进线性化的操作

②线性结构特点——第一个结点有唯一直接后继,最后一个结点有唯一直接前驱,其它结点有唯一直接前驱、直接后继

③传统二叉链表——反应父子关系,无法反应线性关系(直接前驱、直接后继是谁)

④二叉链表表示的树存在n+1个空指针域

线索二叉树的说明:

①利用这n+1个空指针域,保存直接前驱或直接后继的关系

②加快了查找直接前驱、直接后继的速度

③二叉树的非递归遍历省去了系统栈, 线索二叉树将进一步省去用户栈

④线索二见树是一种存储(物理)结构,这里明确说了二叉链表方式进行存储

结点描述:

typedef struct ThreadNode {
	ElemType data;  //数据域
	struct ThreadNode    //左右孩子指针域
		* lchild,
		* rchild;
	int ltag,     //左右线索标志
		rtag;
}ThreadNode, * ThreadTree;

注解:

①由上述结点构成的二叉链表作为二叉树的存储结构,叫作线索链表

②指向结点前驱、后继的指针,叫作线索

③加上线索的二叉树叫作线索二叉树(前序线索二叉树、中序线索二叉树、后序线索二叉树)

④对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化

⑤线索二叉树是存储结构,限定了必须用二叉链表

3.手工画线索二叉树

步骤:

①根据下标法得出相关遍历序列

②根据左右子树情况,连接线索

例子:

 

 

4.中序线索二叉树

(1)中序线索二叉树的构造

构造过程:就是中序遍历一次二叉树,将空指针域填充内容

思想:

1.采用递归方式进行中序遍历
2.建立线索规则左线索指针(为空时)指向当前正在访问的结点的前驱结点(中序遍历)

pre指向直接前驱

p指向当前结点

p左线索(为空时)指向pre

pre右线索(为空时)指向p

右线索指针(为空时)指向当前正在访问的结点的后继结点(中序遍历)

①当遍历到最后一个结点时,pre指向倒数第二个结点,这时候处理了最后一个结点的前驱,但是没有处理最后一个结点的后继,记得特殊处理它

②遍历第一个结点时,它没有pre,对第一个结点的pre要特殊处理

代码:

void createInThread(ThreadTree T) {
	ThreadNode* pre = NULL;
	if (T != NULL) {
		inThread(T, pre);
		//最后一个结点,右子树单独处理
		pre->rchild = NULL;
		pre->rtag = 1;
	}
}

void inThread(ThreadTree& p, ThreadNode*& pre) {
	//当前处理结点不为空
	if (p != NULL) {
		//线索化左子树
		inThread(p->lchild, pre);

		//当前结点p的左子树为空,则存储前驱
		if (p->lchild == NULL) {
			p->lchild = pre;
			p->ltag = 1;
		}

		//前驱结点pre的右子树为空,则存储后继
		if (pre != NULL && pre->rchild == NULL) {
			pre->rchild = p;
			pre->ltag = 1;
		}

		//当前节点已经访问过
		pre = p;

		//线索化左子树
		inThread(p->lchild, pre);
	}
}

注解:

线性化后第一个结点,最后一个结点都有一个空闲指针,可以利用起来

好处——可以从尾巴开始寻找

添加一个头结点——头结点的rchild指针指向线性化后的最后一个结点;头结点的lchild指针指向根结点;线性化后的第一个结点的lchild指向头结点;线性化后的最后一个结点的rchild指向头结点

(2)中序线索二叉树的遍历

利用线索二叉树,可以实现二叉树遍历的非递归算法

思想(中序、无头结点):

第一个结点——最左侧的结点(中序遍历的特点)

求A的后继结点——若A的rchild指针存储的是后继(线索),那么后继就是A的rchild指向的结点;若A的rchild指针存储的是右孩子,那么A的rchild作为根的最左侧结点

注解:

中序遍历下,结点A的直接前驱(有的话)——A的左孩子代表的树的最右侧的那个结点

中序遍历下,结点A的直接后继(有的话)——A的右孩子代表的树的最左侧的那个结点

代码:

①求中序下的第一个结点

//求中序下的第一个结点
ThreadNode* FirstNode(ThreadNode* p) {
	while (p->ltag == 0) { //找最左侧的结点
		p = p->lchild;
	}
	return p;
}

②求结点p的后继结点

//求结点p的后继结点
ThreadNode* nextNode(ThreadNode* p) {
	if (p->rtag == 0) {   //存储的是右孩子
		return FirstNode(p->rchild);
	}
	else {                //存储的是后继
		return p->rchild;
	}
}

③利用线索二叉树进行中序遍历

void inOrder(ThreadNode* T) {
	for (ThreadNode* p = FirstNode(T); p != NULL; p = nextNode(p)) {
		visit(p);
	}
}

5.前序线索二叉树

与中序线索二叉树类似

注解:

①前序遍历PLR

②对前序线索二叉树进行从后往前的遍历时,没有办法直接找到当前结点的直接前驱(因为PLR,P的左右子树中都没有P的直接前驱),这时候还是需要使用栈

③默认提到的遍历都是从前向后,除非特殊说从后向前

6.后序线索二叉树

与中序线索二叉树类似

注解:

①后序遍历LRP

②对后序线索二叉树进行从前往后的遍历时,没有办法直接找到当前结点的直接后继(因为LRP,P的左右子树中都没有P的直接后继),这时候还是需要使用栈

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/656932.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号