二叉树】3.二叉树的遍历、线索二叉树"/>
【Ⅵ树与二叉树】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】思想
【2】注解
①为什么左子树后入栈?因为位于栈顶的元素最先被访问。
②对于子树,就是一棵树,按处理树的完整流程去做就行。
【3】例子
【4】代码
void preOrderNoRecursion(BiTree bt)
{BiTNode* Stack[MaxSize]; // 定义栈int top = -1; // 定义栈顶指针,并初始化为-1BiTNode* 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】思想
【2】注解
①对于子树,就是一棵树,按处理树的完整流程去做就行。
②2.1)思想设计原因见下面表格。
没有右孩子,意味着当前这棵子树已经遍历完毕 | |
当前这棵树没有右孩子并且是上面树的左子树——下面应该访问上面树的根 | |
当前这棵树没有右孩子并且是上面树的右子树——上面整棵树已经访问完毕了,对上面这棵树同理,最终走到整体是某棵树的左子树,或者栈空 |
【3】例子
【4】代码
void inOrderNoRecursion(BiTree bt)
{BiTNode* Stack[MaxSize]; // 定义栈int top = -1; // 定义栈顶指针,并初始化为-1BiTNode* 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】思想
【2】注解
①对于子树,就是一棵树,按处理树的完整流程去做就行。
②2.2)思想设计原因见下面表格。
现象 | 结论 |
当左子树访问完毕后,会回到根结点 | 栈顶结点有右子树且右子树未被访问——右孩子入栈 栈顶结点有右子树且右子树已被访问——访问根结点 |
当右子树访问完毕后,会回到根结点 | |
右子树的访问次序在根结点的前一位(写代码时,用一个指针变量指向前一个被访问的结点就可以) |
③2.3)与2.4)思想设计原因见下面表格。
2.3) | |
2.4) |
【3】例子
【4】代码
void postOrderNoRecursion(BiTree bt)
{BiTNode* Stack[MaxSize]; // 定义栈int top = -1; // 定义栈顶指针,并初始化为-1BiTNode* 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)) {//出队元素,交给pDeQueue(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.二叉树的遍历、线索二叉树
发布评论