【Ⅵ树与二叉树】3.二叉树的遍历、线索二叉树

一、二叉树的遍历

1、遍历

【1】二叉树的遍历

使二叉树中的结点被访问且只被访问一次。

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

前序遍历PLR
中序遍历LPR
后序遍历LRP

【3】注解

①前、中、后指的都是根被访问的次序。

②左右的相对顺序都一样。

③二叉树是一个递归定义。

2、手工遍历二叉树

【1】步骤

①在所有结点上的相应位置上进行标记。

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

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

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

【2】例子

3、前序遍历

方案一:递归实现

【1】思想

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

【2】例子

【3】代码

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

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

方案二:非递归实现

【1】思想

d61d9975cc555fa248a0f6c0df07fa3c.png

【2】注解

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

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

【3】例子

【4】代码

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)将该结点的右孩子入栈
			if (p->rchild != NULL)
			{
				Stack[++top] = p->rchild;
			}

			// 2.3)将该结点的左孩子入栈
			if (p->lchild != NULL)
			{
				Stack[++top] = p->lchild;
			}
		}
	}
}

4、中序遍历

方案一:递归实现

【1】思想

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

【2】例子

【3】代码

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

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

方案二:非递归实现

【1】思想

13f9ceb009ea84e8054351ddde0552d0.png

【2】注解

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

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

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

【3】例子

【4】代码

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

	BiTNode* p;  // 临时指针
	bool rightEmpty = false;  // 标志当前访问的这颗子树有无右孩子

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

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

			if (p->lchild == NULL || rightEmpty)  // 栈顶结点无左孩子或上一棵访问的子树无右孩子
			{
				rightEmpty = false;  // 将标志置为初始值false

				// 栈顶结点出栈并访问之
				top--;
				visit(p);

				// 2.1)观测该结点的右孩子
				if (p->rchild != NULL)  // 该结点有右孩子
				{
					Stack[++top] = p->rchild;   // 右孩子入栈
				}
				else  // 该结点无右孩子
				{
					rightEmpty = true;  // 标志该结点无右孩子,置为true,下次循环直接重复2.1)
				}
			}
			else  // 栈顶结点有左孩子
			{
				Stack[++top] = p->lchild;  // 左孩子入栈
			}
		}
	}
}

5.后序遍历

方案一:递归实现

【1】思想

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

【2】例子

【3】代码

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

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

方案二:非递归实现

【1】思想

827c32bba038edf49c2705b8060672bd.png

【2】注解

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

②2.2)思想设计原因见下面表格。

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

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

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

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

③2.3)与2.4)思想设计原因见下面表格。

2.3)
2.4)

【3】例子

【4】代码

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

	BiTNode* p;  // 临时指针
	BiTNode* pre = NULL;  // 标志当前访问结点之前被访问的结点
	bool visited = false;  // 标志当前访问的这颗子树有无右孩子且是否已被访问

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

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

			if (p->lchild == NULL || visited)  // 栈顶结点无左孩子或上一棵访问的子树无右孩子或者有右孩子且右孩子已被访问
			{
				visited = false;  //将标志置为初始值false

				// 2.2)栈顶结点有右孩子且右孩子未被访问
				if (p->rchild != NULL && p->rchild != pre)
				{
					Stack[++top] = p->rchild;  // 右孩子入栈
				}
				// 2.3)栈顶结点无右孩子
				else if ((p->rchild == NULL)
					// 2.4)栈顶结点有右孩子且右孩子已被访问
					|| (p->rchild != NULL && p->rchild == pre))
				{
					//栈顶元素出栈并访问之
					top--;
					visit(p);

					pre = p;  // 更新pre,标志该结点为下一个访问结点之前被访问的结点
					visited = true;  // 标志该结点无右孩子或者有右孩子且右孩子已被访问,置为true,下次循环直接重复2)且不含2.1)
				}
			}
			// 2.1)栈顶结点有左孩子
			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的直接后继),这时候还是需要使用栈

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aspect of Twilight

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值