多叉树的前序遍历_二叉树的非递归遍历的思考

d41860e90029380cbfe869a08f4590b1.png

封面图来自wikipedia

1 简介

二叉树的深度优先遍历(前序遍历、中序遍历、后序遍历)是一个比较基本的操作。如果使用递归的做法,很容易写出相应的程序;而如果使用非递归的做法,虽然也能写出相应的代码,但是由于三种非递归的遍历没有统一的格式,比较难记住。在这里,介绍一种统一格式的非递归写法。

2 递归做法


先介绍一下二叉树的三个深度优先遍历的基本概念:

  • 前序遍历:先访问根节点,然后前序遍历左子树,最后前序遍历右子树。
  • 中序遍历:先中序遍历左子树,然后访问根节点,最后中序遍历右子树。
  • 后序遍历:先后序遍历左子树,然后后序遍历右子树,最后访问根节点。

根据概念很容易写出对应的递归遍历代码

2.0 数据结构定义

struct TreeNode{TreeNode* left;TreeNode* right;int val;
};

2.1 前序遍历

vector<int> preorder(TreeNode* root, vector<int>& res) {if (!root) return res;res.push_back(root->val);preorder(root->left, res);preorder(root->right, res);return res;
}

2.2 中序遍历

vector<int> inorder(TreeNode* root, vector<int>& res) {if (!root) return res;inorder(root->left, res);res.push_back(root->val);inorder(root->right, res);return res;
}

2.3 后序遍历

vector<int> postorder(TreeNode* root, vector<int>& res) {if (!root) return res;postorder(root->left, res);postorder(root->right, res);res.push_back(root->val);return res;
}

3 非递归做法

先列出代码,后面再写下代码的思想以及自己的理解。

可以看出三种遍历的写法,除了三句执行入栈的代码,顺序不一样,其他都是一致的,实现了格式的统一。

3.1 前序遍历

void preorder(TreeNode *root, vector<int>& res)
{stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));s.push(make_pair(root, true));}}
}

3.2 中序遍历

void inorder(TreeNode *root, vector<int>& res)
{stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root->right, false));s.push(make_pair(root, true));s.push(make_pair(root->left, false));}}
}

3.3 后序遍历

void postorder(TreeNode *root, vector<int>& res)
{stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root, true));s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));}}
}

4 算法思想

4.1 简要说明

下面以前序遍历为例子,简单说说我自己的理解。先总结下自己的理解:

前序遍历的规则:“根节点-左子树递归-右子树递归”,等价于下面两个规则

  1. 对于每个节点,访问顺序为:“节点-左节点-右节点”
  2. 对于每个节点,左子树的节点全部访问完,再开始访问右子树的节点。

4.2 详细解释

接下来尝试对上面的话解释一下。

回看前序遍历的概念,可以发现它制定了遍历的规则:先是根节点,然后递归遍历左子树,最后递归遍历右子树,我们表示成“根节点-左子树-右子树”。这个好像不太直观,我们想想这个规则能不能表示成其他等价规则。首先想到的一点是:

  • (a) 对于树中的每一个节点,它以及它的两个子节点的访问顺序必须是 “节点-左子节点-右子节点”。

这个很容易理解。对于一个节点来说,它的左子节点是左子树的根节点,右子节点是右子树的根节点,既然要求 “节点-左子树-右子树”,那么必要条件就有 “节点-左子节点-右子节点”。其次,递归遍历使得对于每个节点,都有这样的要求。

但是这个只是必要条件,并不能唯一确定节点访问顺序。举个例子,假设有下面一棵二叉树,那么它的前序遍历是 “1-2-4-5-3-6-7”。假设我们只是规定了 “节点-左子节点-右子节点” 这个规则,那么我们便规定了下面三个序列的次序:“1-2-3”、“2-4-5”、“3-6-7”,(即:3 必须在 2 之后访问,2 必须在 1 之后访问...)然而我们没有规定这三个序列之间的相对次序,那么符合条件的次序就有很多了,比如 “1-2-3-4-5-6-7”、“1-2-3-6-7-4-5”,“1-2-4-3-6-5-7” 等等。

56a7bb9670e250345bcdafbdba7ee92e.png
图1 - 二叉树例子

仔细思考了一下,出现上面这些序列的原因是:我们没有规定左子树 “2-4-5” 与右子树 “3-6-7” 两个子树之间的相对顺序。比如第一个例子 “1-2-3-4-5-6-7”,在左子树只访问根节点 “2” 之后,就去访问右子树的根节点 “3”,之后再访问左子树剩下的部分,最后再访问右子树剩下的部分。

我们知道正确的做法是:先访问完所有左子树的节点,再访问所有右子树的节点。于是得到第二条规则:

  • (b) 对于树中的每一个节点,只有当左子树的节点全部访问完,才能访问右子树的节点。

有了上述两条规则,遍历顺序便被唯一确定了。当然我不知道怎么严谨地证明这个结论。

回头再思考一下上面两个规则,第一个规则规定了节点与它的两个子节点(子树)之间的顺序,而第二个规则规定了两个子树之间的顺序。

5 代码对算法的实现

来看看代码怎么实现我们上面说的两点规则的。为了方便,我把代码搬了下来。

// 前序遍历
void preorder(TreeNode *root, vector<int>& res)
{stack< pair<TreeNode*, bool> > s;s.push(make_pair(root, false));bool visited;while(!s.empty()) {root = s.top().first;visited = s.top().second;s.pop();if(root == NULL) {continue;}if(visited) {res.push_back(root->val);} else {s.push(make_pair(root->right, false));s.push(make_pair(root->left, false));s.push(make_pair(root, true));}}
}
  • (1) 首先注意到,代码使用了栈,在元素入栈的时候,三条语句确定了一个节点与它的两个子节点之间的顺序。对所有的节点进行这个操作,便实现了规则(a)。
  • (2) 由于栈的 “后进先出” 特性,根据入栈的顺序,相比左子节点,右子节点会在栈更深的位置,所以后续会先访问左子节点。访问左子节点的时候,会将它的子节点压入栈,因此所有的左子树的节点都会比原本右子节点更先访问到。因此,栈的本身结构保证了所有的节点都执行了规则(b)。
  • (3) 代码中对每个节点使用了一个标记位,开始第一次入栈时,都标记为 false,只有当第二次入栈时,节点以及它的子节点顺序确定,才被标记成 true。换句话说,false 表示了当前节点与其子节点的顺序还没确定下来,true 表示当前节点与其子节点的顺序已经确定下来,因此可以被访问了。这个保证了树中的 “所有” 节点都执行了规则 (a)。

下面是算法执行的示意图,便于大家理解算法流程。

f9442dc64e0077d9cdb4ae69099856b6.png
图2 - 前序遍历流程图

7 总结

我们将树的遍历的规则转化为两条等价的规则,其中一条确定了节点与子节点之间的遍历顺序,另一条确定了子节点之间的遍历顺序。之后,借助栈的特性,实现了上述两条规则,即实现了树的遍历。

算法的优点是将遍历顺序与算法逻辑之间的分离,于是使用哪一种遍历顺序,不影响算法本身的逻辑。换一句话说,不管是哪一种遍历顺序,代码的整体框架是一样的,只需稍微改变跟顺序相关的几句代码,就ok了。除此之外,很容易推广到多叉树。

算法的缺点嘛,对于每个节点都需要入栈两次,同时对于每个节点都需要分配一个标志位,但是我觉得瑕不掩瑜。

8 参考资料

在写作的过程中,参考了以下一些资料,在此表示感谢

https://blog.csdn.net/sdulibh/article/details/50573036

自己水平有限,哪里写错了,欢迎指正,虚心接受大家的意见。

如果觉得我的文章对你有帮助,欢迎点赞、收藏、关注呀,以激励我更好地分享呀~

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

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

相关文章

delphi中Label中文显示不全的问题解决办法

有时候把Label的AutoSize属性设置为True&#xff0c;当窗体显示的时候&#xff0c;Label中的内容可能会显示不完全&#xff0c;只能把AutoSize设置为False&#xff0c; 把Label调整成能显示出内容的大小。还有一种更简单的解决方法。把Form的Font属性进行如下设置&#xff1a;字…

焊接空间臂_焊接烟尘净化器设备哪种好

焊接烟尘净化器设备采用滤筒除尘器&#xff0c;焊接烟尘净化器用于焊接、切割、打磨等工序中产生烟尘和粉尘的净化以及对稀有金属、贵重物料的回收等&#xff0c;可净化大量悬浮在空气中对人体有害的细小金属颗粒。具有净化效率高、噪声低、使用灵活、占地面积小等特点。 适用于…

【摘录】C语言中利用 strtok函数进行字符串分割

C语言不像Java,Php之类的高级语言&#xff0c;对象中直接封装了字符串的处理函数。C语言中进行普通的字符串处理也经常会让我们焦头烂额……不过好在C语言 中还是提供了像strtok这样功能强大的字符串处理函数&#xff0c;可以帮我们实现部分需要的功能。下面我们介绍一下strtok…

woe分析_Python数据分析—apply函数

在对海量数据进行分析的过程中&#xff0c;我们可能要把文本型的数据处理成数值型的数据&#xff0c;方便放到模型中进行使用。也可能需要把数值型的数据分段进行处理&#xff0c;比如变量的woe化。而这些操作都可以借助python中的apply函数进行处理。今天介绍数据分析的第四课…

树莓派3b安装ubuntu mate(在有显示器前提下看)

树莓派安装&#xff1a; 准备材料 tf卡&#xff08;建议16G&#xff09;数据线树莓派win32烧录软件 &#xff0c;百度云链接&#xff1a;链接&#xff1a;https://pan.baidu.com/s/16Dq2XrqeJScUO_DxHRIz_g 提取码&#xff1a;kfkbubtuntu mate系统&#xff08;建议不要下ubu…

打包mac应用_把网址链接打包成电脑软件的制作方法

前言&#xff1a;学习一下把web页面打包成运行在桌面的应用, 并支持win / mac / linux 等平台, 记一下使用过程, 有需要的大(同)佬(学)可以玩玩~第一步 – 安装 node.jsnode.js下载地址&#xff1a;http://nodejs.cn/download/下载 Windows 安装包 (.msi) 和 Windows 二进制文件…

对多个WCF服务进行统一的连接测试

先看下面的代码&#xff1a;代码代码 BasicHttpBinding myBinding newBasicHttpBinding(); EndpointAddress myEndpoint newEndpointAddress(endAddress); ChannelFactory<IMyService>myChannelFactory newChannelFactory<IMyService>(myBinding,my…

宜昌宝塔河项目_宜昌城区首个垃圾分类定时定点投放点启用 厨余垃圾破袋投放...

伍家岗区宝联社区黄龙小区的垃圾分类定时定点投放点启用。(市环境卫生管理处供图)(记者郑璐、通讯员陈赞)1月1日&#xff0c;宜昌城区首个垃圾分类定时定点投放点在伍家岗区宝塔河街办宝联社区黄龙小区正式启用。该投放点每天开放5小时&#xff0c;上午7&#xff1a;00-9:30&am…

装配图位置偏转怎么调整_物理微课|匀变速直线运动、电容器动态分析及磁偏转技巧、方法、模型...

匀变速直线运动三大推论是什么&#xff1f;如何利用它们快速解题&#xff1f;电容器动态分析的重点是什么&#xff1f;磁偏转问题有什么严谨好用的技巧和方法&#xff1f;物理侯老师为您详细解答以上问题。高一匀变速直线运动三大推论 匀变速直线运动是我们高中学的第一个变速…

Cooki模拟登陆(人人网)

我们在爬取网上一些数据时&#xff0c;必须登陆才能爬取到数据&#xff0c;这是我们就需要Cookie了&#xff0c;Cookie简单说就是服务器返回给我们的一些数据&#xff0c;保存到客户端&#xff0c;下次登陆时&#xff0c;服务器会识别这些数据&#xff0c;可以返回我们上次的数…

基本图形怎么改字体_PPT做得慢怎么办?掌握这6个技巧,你也能快速做出精美的PPT...

相信很多人做PPT的速度都比较慢&#xff0c;从新建一个空白PPT开始&#xff0c;再到排版设计&#xff0c;需要耗费大量的时间&#xff0c;下面就来教你这6个技巧&#xff0c;你也能快速做出精美的PPT。01.一键禁止动画最近&#xff0c;有很多小伙伴向我求助&#xff0c;问我怎样…

三菱880彩铅和uni的区别_孟祥雷丨清华美院毕业,彩铅界的“冷军”(附彩铅教程哦!)...

今天要分享的是妥妥的一个高冷帅气、又有才华的艺术家&#xff0c;毕业于清华大学美术学院。有20多年的绘画经验&#xff0c;设计、绘画等艺术多面手&#xff0c;彩铅是他最出名的作品之一。人物篇(逆光)(麻花辫)用彩铅画肖像人物的人很多&#xff0c;但能把人物塑造得维妙维俏…

substring()分解字符串

substring解决了如何在指定位置将一个字符串划分为子串 单参数形式&#xff1a;返回从起始位置到结尾之间的子串**&#xff08;起始索引从0开始&#xff09;** public class SubStringReview {public static void main(String[] args) {String s "Java is great";…

11g oracle xe启动_详解Oracle等待事件的分类、发现及优化

一、等待事件由来大家可能有些奇怪&#xff0c;为什么说等待事件&#xff0c;先谈到了指标体系。其实&#xff0c;正是因为指标体系的发展&#xff0c;才导致等待事件的引入。总结一下&#xff0c;Oracle的指标体系&#xff0c;大致经历了下面三个阶段&#xff1a;以命中率为主…

StringTokenizer将一个字符串分解为单词或者标记

原理&#xff1a;StringTokenizer方法实现了Iterator的设计模式&#xff0c;也直接实现了枚举接口&#xff0c;通常情况下StringTotkenizer对象根据欧洲语言的单词分割将对字符串分解为若干单词&#xff0c;例如&#xff1a; public class StringTokenizerReview {public stat…

hystrix 单独使用_Hystrix学习

学习主题&#xff1a;Hystrix解决灾难性雪崩效应-服务熔断-服务熔断处理熔断参数circuitBreaker.enabled的作用是什么&#xff1f;熔断参数circuitBreaker.requestVolumeThreshold的作用是什么&#xff1f;熔断参数circuitBreaker.sleepWindowInMiliseconds的作用是什么&#x…

WebClient与WebRequest差异

WebRequst的使用 WebClient和HttpWebRequst是用来获取数据的2种方式&#xff0c;在我的这篇数据访问(2)中主要是讲的WebClient的使用&#xff0c;一般而言&#xff0c;WebClient更倾向于“按需下载”&#xff0c;事实上掌握它也是相对容易的&#xff0c;而HttpWebRequst则允许你…

连接字符串

方式一&#xff1a;可以使用运算符**“”**来连接字符串 public class StringBufferReview {/*** 方式一&#xff1a;使用连接* param args*/public static void main(String[] args) {String s1 "hello";String s2 "Java";String s3 s1s2;System.out.…

的使用_面部精华使用方法和使用步骤;

核心提示&#xff1a;精华液&#xff0c;是护肤品中之极品&#xff0c;成分精致、功效强大、效果显著&#xff0c;始终保持着它拥有的高贵和神秘。精华液中的基质是水&#xff0c;含有硅树脂&#xff0c;有利于渗透进皮肤和推开&#xff0c;其它添加成分&#xff0c;则带有治疗…

处理字符串中的单个字符CharAt()

1、解决方法&#xff1a;采用循环以及String类的charAt()方法 charAt()方法将返回String对象中索引值**&#xff08;从0开始&#xff09;**位置的字符。所以&#xff0c;我们只需要执行从0到String.length()-1的循环&#xff0c;就能够依次处理字符串中的所有字符。 注意&#…