二叉树的先序遍历、中序遍历、后序遍历、层次遍历-图文详解

概述

二叉树的遍历是一个很常见的问题。二叉树的遍历方式主要有:先序遍历、中序遍历、后序遍历、层次遍历。先序、中序、后序其实指的是父节点被访问的次序。若在遍历过程中,父节点先于它的子节点被访问,就是先序遍历;父节点被访问的次序位于左右孩子节点之间,就是中序遍历;访问完左右孩子节点之后再访问父节点,就是后序遍历。不论是先序遍历、中序遍历还是后序遍历,左右孩子节点的相对访问次序是不变的,总是先访问左孩子节点,再访问右孩子节点。而层次遍历,就是按照从上到下、从左向右的顺序访问二叉树的每个节点。

在介绍遍历算法之前,先定义一个二叉树的结构体。使用的是 C++ 语言。

//filename: BinTreeNode.h
template <typename T> struct BinTreeNode {T data; //数据域
    BinTreeNode * LeftChild; //左孩子节点指针
    BinTreeNode * RightChild; //右孩子节点指针
    BinTreeNode * parent; //父节点指针
};

先序遍历

递归

使用递归,很容易写出一个遍历算法。代码如下:

//filename: BinTreeNode.h
template <typename T>
void travPre_R(BinTreeNode<T> * root) {//二叉树先序遍历算法(递归版)
    if (!root) return;cout << root->data;travPre_R(root->LeftChild);travPre_R(root->RightChild);
}

迭代

在之前的文章中,我不止一次地说过,递归是很耗费计算机资源的,所以我们在写程序的时候要尽量避免使用递归。幸运的是,绝大部分递归的代码都有相应的迭代版本。那么我们就试着将上述递归代码改写成迭代的版本。改写之后,代码如下:

//filename: BinTreeNode.h
template <typename T>
void travPre_I1(BinTreeNode<T> * root) {//二叉树先序遍历算法(迭代版#1)
    Stack<BinTreeNode<T>*> s; //辅助栈
    if (root) //如果根节点不为空
        s.push(root); //则令根节点入栈
    while (!s.empty()) { //在栈变空之前反复循环
        root = s.pop(); cout << root->data; //弹出并访问当前节点
//下面左右孩子的顺序不能颠倒,必须先让右孩子先入栈,再让左孩子入栈。
        if (root->RightChild)s.push(root->RightChild); //右孩子先入后出
        if (root->LeftChild)s.push(root->LeftChild); //左孩子后入先出
    }
}

下面我们通过一个实例来了解一下该迭代版本是如何工作的。

PS:黑色的元素表示已经被弹出并访问过。

结合代码,该二叉树的先序遍历过程如下:

  1. 初始化一个空栈。
  2. 根节点入栈,此时将 a 入栈。
  3. 循环开始,弹出并访问栈顶元素,此时栈顶元素是 a。
  4. 如果 a 有右孩子,则将其右孩子节点入栈;如果 a 有左孩子,则将其左孩子节点入栈。此时栈中有 b、c 两个元素。
  5. 这时进入下一轮循环。弹出并访问栈顶元素,此时栈顶元素是 b。经检查,b 没有右孩子,也没有左孩子,进入下一轮循环。
  6. 弹出并访问栈顶元素,此时栈顶元素是 c。c 的右孩子是 f,左孩子是 d,故分别将 d、f 入栈。进入下一轮循环。
  7. 此时栈中的元素是 d、f。
  8. 弹出并访问栈顶元素,此时栈顶元素是 d。d 的右孩子是 e,d 没有左孩子,故将 e 入栈。进入下一轮循环。
  9. 此时栈中的元素是 e、f。
  10. 弹出并访问栈顶元素,此时栈顶元素是 e。e 没有左右孩子,进入下一轮循环。
  11. 弹出并访问栈顶元素,此时栈顶元素是 f。f 没有左右孩子,进入下一轮循环。
  12. 此时栈为空,退出循环。遍历结束。

这个迭代的遍历算法非常简明,但是很遗憾,这种算法并不容易推广到我们接下来要研究的中序遍历和后序遍历。因此我问需要寻找另一种策略。

第 2 种迭代方式

我们来看一个规模更大、更具一般性的二叉树:

这个二叉树的先序遍历序列是:idcabhfeglkjnmpo,也就是遵循了下图所示的顺序:

再进一步,我们把二叉树抽象成下面这个样子,


L_0 ~ L_d 是二叉树的左侧链上的节点, R_0 ~ R_d 分别是 L_0 ~ L_d 的右孩子,T_0 ~ T_d 分别是 L_0 ~ L_d 的右子树。不难发现,二叉树的先序遍历就是先自上而下访问左侧链上的节点,再自下而上访问左侧链上的节点的右子树。而我们的遍历算法,就是根据这样一个思路来进行设计。

首先需要实现一个子方法,就是访问二叉树左侧链上的节点:

//从当前节点出发,沿左分支不断深入,直至没有左分支的节点;沿途节点遇到后立即访问
template <typename T> //元素类型、操作器
static void visitAlongLeftBranch ( BinTreeNode<T>* x, Stack<BinTreeNode<T>*>& S ) {while ( x ) {cout << x->data; //访问当前节点
      if( x->RightChild )S.push ( x->RightChild ); //右孩子入栈暂存(可优化:通过判断,避免空的右孩子入栈)
      x = x->LeftChild;  //沿左分支深入一层
   }
}

然后是主方法,在主方法中,通过迭代,不断地调用上面这个子方法,从而实现完整的二叉树先序遍历。

template <typename T> //元素类型、操作器
void travPre_I2 ( BinTreeNode<T>* root) { //二叉树先序遍历算法(迭代版#2)
   Stack<BinTreeNode<T>*> S; //辅助栈
   while ( true ) {visitAlongLeftBranch ( root, S ); //从当前节点出发,逐批访问
      if ( S.empty() ) break; //直到栈空
      root = S.pop(); //弹出下一批的起点
   }
}

中序遍历

递归

与先序遍历类似,递归版的中序遍历算法很容易实现,代码如下:

template <typename T>
void travIn_R(BinTreeNode<T> * root) {//二叉树先序遍历算法(递归版)
    if (!root)return;travPre_R(root->LeftChild);cout << root->data;travPre_R(root->RightChild);
}

递归代码不仅容易实现,也很好理解,这里不再做过多解释。

迭代

参照迭代式先序遍历版本 2 的思路,在宏观上,我们可以将中序遍历的顺序抽象为,先访问二叉树的左侧链上的最底部的节点,然后访问该节点的右子树(如果有的话),然后访问该节点的父节点,然后访问该节点的父节点的右子树(如果有的话)……直至全部节点被访问完毕。如下图所示:

按照以上思路,可以实现迭代版中序遍历算法如下:

template <typename T> //从当前节点出发,沿左分支不断深入,直至没有左分支的节点
static void goAlongLeftBranch ( BinTreeNode<T> * x, Stack<BinTreeNode<T> * >& S ) {while (x) { S.push(x); x = x->LeftChild; } //当前节点入栈后随即向左侧分支深入,迭代直到无左孩子
}

template <typename T> //元素类型、操作器
void travIn_I(BinTreeNode<T> root) {//二叉树先序遍历算法(迭代版)
Stack<BinTreeNode<T> > S; //辅助栈
while ( true ) {
goAlongLeftBranch ( root, S ); //从当前节点出发,逐批入栈
if ( S.empty() ) break; //直至所有节点处理完毕
root = S.pop();
cout << root->data; //弹出栈顶节点并访问之
root = root->RightChild; //转向右子树
}
}

也可以对代码稍加改进,将这两个方法写成一个方法:

template <typename T> //元素类型
void travIn_I2 ( BinTreeNode<T> root ) { //二叉树中序遍历算法(迭代版#2)
Stack<BinTreeNode<T>> S; //辅助栈
while ( true )
if ( root ) {
S.push ( root ); //根节点进栈
root = root->LeftChild; //深入遍历左子树
} else if ( !S.empty() ) {
root = S.pop(); //尚未访问的最低祖先节点退栈
cout << root->data; //访问该祖先节点
root = root->RightChild; //遍历祖先的右子树
} else
break; //遍历完成
}

后序遍历

递归

与前两个一样,二叉树的后序遍历算法可以很容易地用递归的方式实现。

template <typename T>
void travPost_R(BinTreeNode<T> root) {//二叉树先序遍历算法(递归版)
if (!root)
return;
travPost_R(root->LeftChild);
travPost_R(root->RightChild);
cout << root->data;
}

迭代

但是要想用迭代的方式实现后序遍历算法,则有一定的难度,因为左、右子树的递归遍历均严格地不属于尾递归。不过,仍可继续套用此前的思路和技巧,考虑一下,后序遍历中,首先访问的是哪个节点?答案就是二叉树的最高最左侧的叶子节点。

由于最高最左侧的叶子节点 V 可能是左孩子节点,也可能是右孩子节点,所以 V 与其父节点之间的联接用竖直的线表示。考查联接于 V 与树根之间的唯一通路(以粗线示意)。与先序与中序遍历类似地,自底而上地沿着该通路,整个后序遍历序列也可以分解为若干个片段。每一片段,分别起始于通路上的一个节点,并包括三步:访问当前节点,遍历以其右兄弟(若存在)为根的子树,以及向上回溯至其父亲节点(若存在)并转入下一片段。

基于以上理解,即可写出迭代式后序遍历算法。

template <typename T> //在以S栈顶节点为根的子树中,找到最高左侧叶节点
static void gotoHLVFL ( Stack<BinTreeNode<T>>& S ) { //沿途所遇节点依次入栈
while ( BinTreeNode<T>* x = S.top() ) //自顶而下,反复检查当前节点(即栈顶)
if ( x->LeftChild ) { //尽可能向左
if ( x->RightChild ) S.push ( x->RightChild ); //若有右孩子,优先入栈
S.push ( x->LeftChild ); //然后才转至左孩子
} else //实不得已
S.push ( x->RightChild ); //才向右
S.pop(); //返回之前,弹出栈顶的空节点
}

template <typename T>
void travPost_I ( BinTreeNode<T> root ) { //二叉树的后序遍历(迭代版)
Stack<BinTreeNode<T>> S; //辅助栈
if ( root ) S.push ( root ); //根节点入栈
while ( !S.empty() ) {
if ( S.top() != root->parent ) //若栈顶非当前节点之父(则必为其右兄),此时需
gotoHLVFL ( S ); //在以其右兄为根之子树中,找到HLVFL(相当于递归深入其中)
root = S.pop(); cout << root->data; //弹出栈顶(即前一节点之后继),并访问之
}
}

层次遍历

在文章开头我们已经对层次遍历做了介绍,层次遍历严格按照自上而下、自左向右的顺序访问树的节点。所以我们需要用队列作为辅助,具体代码如下:

template <typename T> //元素类型
void travLevel ( BinTreeNode<T> root ) { //二叉树层次遍历算法
Queue<BinTreeNode<T>> Q; //辅助队列
Q.enqueue ( root ); //根节点入队
while ( !Q.empty() ) { //在队列再次变空之前,反复迭代
BinTreeNode<T>* x = Q.dequeue(); cout << x->data; //取出队首节点并访问之
if ( x->LeftChild ) Q.enqueue ( x->LeftChild ); //左孩子入队
if ( x->RightChild ) Q.enqueue ( x->RightChild ); //右孩子入队
}
}

好了,以上就是二叉树的几种常见的遍历方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/481176.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

写给我的女神,一个用灵魂歌唱的小精灵

我对娱乐圈不感兴趣&#xff0c;也很少关注娱乐圈。对于专业的歌唱艺术更是不在行。但我认为&#xff0c;一首歌&#xff0c;不应因为技法的平庸而被认为是烂歌&#xff0c;不应因为曲风的通俗而被认为是俗歌&#xff0c;不应因为声音和唱法的非主流而被认为是“非主流”歌曲。…

完美世界2020编程题-救雅典娜 英雄AB PK

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请注明出处。 https://blog.csdn.net/u012319493/article/details/82154113 </div><link rel"stylesheet" href"https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-f…

训练神经网络时如何确定batch size?

前言 当我们要训练一个已经写好的神经网络时&#xff0c;我们就要直面诸多的超参数了。这些超参数一旦选不好&#xff0c;那么很有可能让神经网络跑的还不如感知机。因此在面对神经网络这种容量很大的model前&#xff0c;是很有必要深刻的理解一下各个超参数的意义及其对model的…

腾讯2013实习生笔试题+答案1-5aadaa 6-10adbcc 11-15 acacc16-20 bbddc

一、 单项选择题1) 给定3个int类型的正整数x&#xff0c;y&#xff0c;z&#xff0c;对如下4组表达式判断正确的选项(A) Int a1xy-z; int b1x*y/z;Int a2x-zy; int b2x/z*y;int c1x<<y>>z; int d1x&y|z;int c2x>>z<<y; int d2x|z&y;A) a1一定等…

训练神经网络时如何确定batch的大小?

当我们要训练一个已经写好的神经网络时&#xff0c;我们就要直面诸多的超参数啦。这些超参数一旦选不好&#xff0c;那么很有可能让神经网络跑的还不如感知机。因此在面对神经网络这种容量很大的model前&#xff0c;是很有必要深刻的理解一下各个超参数的意义及其对model的影响…

【论文翻译】学习新闻事件预测的因果关系

一、摘要 本文在这项工作中解决的问题是产生一个可能由给定事件引起的可能的未来事件。 论文提出了一种使用机器学习和数据挖掘技术建模和预测未来新闻事件的新方法。论文的Pundit算法概括了因果关系对的例子&#xff0c;以推断因果关系预测因子。为了获得精确标记的因果关系示…

阿里内推算法岗位编程笔试题

版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 https://blog.csdn.net/u014744127/article/details/79431847 </div><link rel"stylesheet" href"https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_v…

从逻辑回归到最大熵模型

在《逻辑回归》与《sigmoid与softmax》中&#xff0c;小夕讲解了逻辑回归背后藏着的东西&#xff0c;这些东西虽然并不是工程中实际看起来的样子&#xff0c;但是却可以帮助我们很透彻的理解其他更复杂的模型&#xff0c;以免各个模型支离破碎。本文中&#xff0c;小夕将带领大…

【论文翻译】统一知识图谱学习和建议:更好地理解用户偏好

一、摘要 将知识图谱&#xff08;KG&#xff09;纳入推荐系统有望提高推荐的准确性和可解释性。然而&#xff0c;现有方法主要假设KG是完整的并且简单地在实体原始数据或嵌入的浅层中转移KG中的“知识”。这可能导致性能欠佳&#xff0c;因为实用的KG很难完成&#xff0c;并且…

机器学习与深度学习常见面试题

为了帮助参加校园招聘、社招的同学更好的准备面试&#xff0c;SIGAI整理出了一些常见的机器学习、深度学习面试题。理解它们&#xff0c;对你通过技术面试非常有帮助&#xff0c;当然&#xff0c;我们不能只限于会做这些题目&#xff0c;最终的目标是真正理解机器学习与深度学习…

EJB的相关知识

一、EJB发展历史 IBM、SUN公司力推EJB前景&#xff0c;大公司开始采用EJB部署系统。主要价值&#xff1a;对分布式应用进行事务管理 出现问题&#xff1a; ①EJB的API难度大 ②规范要求必须抛出特定异常的接口并将Bean类作为抽象类实现&#xff08;不正常不直观&#xff09; ③…

深度前馈网络与Xavier初始化原理

前言 基本的神经网络的知识&#xff08;一般化模型、前向计算、反向传播及其本质、激活函数等&#xff09;小夕已经介绍完毕&#xff0c;本文先讲一下深度前馈网络的BP过程&#xff0c;再基于此来重点讲解在前馈网络中用来初始化model参数的Xavier方法的原理。 前向 前向过程很…

线性代数应该这样讲(三)-向量2范数与模型泛化

在线性代数&#xff08;一&#xff09;中&#xff0c;小夕主要讲解了映射与矩阵的关系&#xff1b;在线性代数&#xff08;二&#xff09;中&#xff0c;小夕讲解了映射视角下的特征值与特征向量的物理意义。本文与下一篇会较为透彻的解析一下向量的二范数与一范数&#xff0c;…

SOA基础

一、架构的演化&#xff1a; 结构化 客户端-服务端 三层 N层 分布式对象 组件 服务&#xff1a;是应用程序或者企业的不同功能单元&#xff0c;每个功能单元作为实例存在&#xff0c;并与应用程序和其他组件交互。通过基于消息的松散耦合的通信模型提供服务。 二、体系结…

从点到线:逻辑回归到条件随机场

开篇高能预警&#xff01;本文前置知识&#xff1a;1、理解特征函数/能量函数、配分函数的概念及其无向图表示&#xff0c;见《逻辑回归到受限玻尔兹曼机》和《解开玻尔兹曼机的封印》&#xff1b;2、理解特征函数形式的逻辑回归模型&#xff0c;见《逻辑回归到最大熵模型》。从…

WSDL基础知识

一、WSDL的定义 将网络服务描述为对包含面向文档或过程的信息进行操作的一组端点的XML格式 服务接口 访问规范 服务地点 定义Web服务的公共接口&#xff08;包括功能、如何调用&#xff09; 定义与目录中列出的Web服务交互所需的协议绑定和消息格式 抽象地描述了支持的操…

【NLP】Google BERT详解

版权声明&#xff1a;博文千万条&#xff0c;版权第一条。转载不规范&#xff0c;博主两行泪 https://blog.csdn.net/qq_39521554/article/details/83062188 </div><link rel"stylesheet" href"https://csdnimg.cn/release/phoenix/template/cs…

有时候,也想过回到过去

人的一生中&#xff0c;总要走走停停。一面向着诗和远方&#xff0c;一面转过身&#xff0c;缅怀过去。她喜欢女生&#xff0c;帅气的女生。我觉得她也很帅&#xff0c;帅气又可爱。初入大学&#xff0c;竞选班委。上台的人中&#xff0c;有阳光幽默的男生&#xff0c;有温柔甜…

SOAP基础知识

一、SOAP是什么&#xff1f; SOAP是一种轻量级协议&#xff0c;旨在在分散的分布式环境中交换结构化信息。 SOAP使用XML技术来定义可扩展的消息传递框架&#xff0c;该框架提供了可以在各种基础协议之间交换的消息构造。 通信协议 用于应用程序之间的通信 发送消息的格式 设…

UDDI基础知识

一、什么是UDDI UDDI基于一组常见的行业标准&#xff0c;包括HTTP&#xff0c;XML&#xff0c;XML Schema和SOAP&#xff0c;为基于Web服务的软件环境提供了一个可互操作的基础基础结构&#xff0c;用于可公开使用的服务和仅在组织内部公开的服务。 仅当潜在用户发现足以允许其…