目录

  • 二叉树的递归与非递归遍历详解
    • 遍历形式
    • 图解先、中、后序遍历
    • 递归实现先、中、后序遍历
    • 非递归实现先、中、后序遍历
    • 层序遍历

二叉树的递归与非递归遍历详解

二叉树是数组结构中非常重要的一种数据类型,很多数据结构与算法都是基于二叉树来进行扩展的,比如树,图等。而数据结构最重要的功能无非就是增删改查等操作,所以本篇文章主要来说说二叉树的查找怎么查找,完整代码可在我的github上查看。

本文中的左子节点和右子节点等同于左节点和右节点。

遍历形式

对于二叉树来说,比较重要的有三种遍历方式:

  1. 先序遍历:按照中间节点、左子树然后右子树的顺序遍历
  2. 中序遍历:按照左子树、中间节点然后右子树的顺序遍历
  3. 后序遍历:按照左子树、右子树然后中间节点的顺序遍历

可以看出这三种遍历的命名其实是根据中间节点的访问时机来命名的,然后这边再补充一种遍历方式–层序遍历,层序遍历就是从根节点开始,从上往下,从左往右进行遍历,按个人理解来说,层序遍历就是二叉树的广度优先搜索,先序遍历就是二叉树的深度优先搜索。

图解先、中、后序遍历

我们先来看看最容易理解的先序遍历是如何运行的,如下图:

上面说到先序遍历就是按照中间节点、左子树然后右子树的顺序遍历,所以从根节点,也就是图中的节点1,然后是节点1的左子树的根节点,也就是节点2,对于节点2来说,也是中左右的顺序遍历,所以节点2访问完以后就访问其左子树的根节点4,而4是叶子节点,没有子节点,则访问完节点4后,退回节点2访问节点2的右子树的根节点也就是5,访问完节点5后,同样因为节点5没有子节点,退回节点2,因为节点2的左右子树都已经访问过了,所以退回到节点1,访问节点1的右子树。以此类推,最后得到的访问顺序就是:1245367。

中序遍历运行过程如下图:

中序遍历是按照左子树、中间节点然后右子树的顺序遍历的,从根节点1开始,先寻找左子树根节点2,对于根节点2来说也同样需要先寻找其左子树的根节点也就是节点4。而节点4是叶子节点,无子节点,所以节点4就是这颗二叉树中序遍历的起点。也就是中序遍历的起始点是从根节点一直往左子树找到的第一个叶子节点(这句话不严谨,如果没有节点4,则是从节点2开始,但是这样更方便理解,而且这种情况下可以把节点4看成一个空节点)。那么剩下的过程和先序遍历类似,所得出的遍历顺序是:4251637。

后序遍历运行过程如下图:

后序遍历是按照左子树、右子树然后中间节点的顺序遍历,和中序遍历一样,先一直往左找到底,如果能找到叶子节点的话,就从该节点开始,如上图就是节点4。但是不同的是,如果没有节点4的话,那么就从节点5继续往左找,当然因为这个图中节点5是叶子节点,所以节点5就是起始节点(没有节点4的话)。那么上面的二叉树的后序遍历的顺序就是:4526731。

后序遍历的过程理解起来比较绕了点,需要稍微多花点时间理解理解。

递归实现先、中、后序遍历

递归实现先、中、后序遍历是很简单的,不过递归在人脑中模拟起来相当费劲,所以最好的办法就是把下面的模板记住

traverse (root) {// 终止条件if (!root) return/** 先序遍历 */// console.log(root.val)// 遍历左子树this.traverse(root.left)/** 中序遍历 */// console.log(root.val)// 遍历右子树this.traverse(root.right)/** 后序遍历 */// console.log(root.val)}

从代码中,我们可以很清晰地看到,代码主要由三部分构成:

  1. 终止条件
  2. 对根节点进行相应的处理
  3. 递归遍历子树

而三种遍历的不同点在于对根节点进行处理的时机的不同。先序遍历是在依次遍历左右子树之前就对根节点进行处理;中序遍历是先遍历左子树,再对根节点进行处理,最后再遍历右子树;而后序遍历则是先依次遍历左右子树后再对根节点进行处理。

其实递归遍历代码很容易理解和记忆,但是其递归过程是一种机械式的运行过程,比较难以被人脑理解,所以一时间无法理解其递归过程的话,可以先记住该模板,而不去理解其过程。

非递归实现先、中、后序遍历

我们捋捋上面三种遍历的递归顺序会发现,三种遍历方式都是从根节点开始,往左子树遍历到底,然后返回父节点判断是否有右子树,再对右子树的根节点进行同样的操作。只是三种遍历方式的读取(处理)数据的时机不同罢了。先将左子树遍历到底,再从后面一个个搜索回来,这不就是典型的后进先出的顺序嘛,所以这里我们借助栈来实现三种遍历方式的非递归算法。
那么,如何使用栈来实现呢?首先创建一个栈,然后将根节点放入,然后依次将其左节点放入栈中,直到左节点为空。从栈中弹出一个节点,判断其是否有右节点,如果有右节点的话,将右节点放入栈中,然后依次将其左节点放入栈中,如此循环,直到栈为空,代码示意如下:

traverse_no_recursion (root) {// 这样声明是为了下面不做太多的初始判断var stack = [null]// 声明一个指向根节点的指针var p = rootwhile (stack.length > 0) {// 将所有左节点放入stack中while (p) {stack.push(p)p = p.left}p = stack.pop()// 弹出栈尾元素,将p指向其右节点(可能为null,所以先判断下)p = p && p.right} }

以上便是非递归的遍历模板,下面讲讲三种遍历方式的具体实现:

  1. 先序遍历:每次将节点放入栈之前,就读取(操作)值即可。
  2. 中序遍历:每次节点从栈中弹出之后,读取(操作)其值即可。
  3. 后序遍历:后序遍历稍微复杂点,但是只要脑子稍微绕下弯就会发现,在每次节点从栈中弹出之前,就直接读取其右节点压入栈中,这样右子树就会比跟节点先弹出来(后进先出),这样同样在节点从栈中弹出之后,读取(操作)其值即可。

先、中序遍历实现:

traverse_no_recursion (root) {var stack = [null]var p = rootwhile (stack.length > 0) {// 将所有左节点放入stack中while (p) {// 先序遍历// console.log(p.val)stack.push(p)p = p.left}p = stack.pop()// 中序遍历// console.log(p && p.val)p = p && p.right} }

后序遍历的思路实现:

traverse_no_recursion_LRD (root) {var stack = [null]var p = rootwhile (stack.length > 0) {// 将所有左节点放入stack中while (p) {stack.push(p)p = p.left}// 不弹出栈尾元素,直接读取其右节点var lastNode = stack[stack.length - 1]if (lastNode && lastNode.right) {// 如果右节点存在,则将p指向右节点等待放入栈中p = lastNode.right} else {// 如果右节点不存在,则节点开始出栈var node = stack.pop()console.log(node.val)}
}

上面的程序运行你会发现报错,因为访问完右节点到底之后,弹出节点前又会判断右节点是否存在然后放入栈中,从而陷入死循环,所以这里我们需要加个处理,给节点添加一个是否访问过的标志(不想污染数据的话,可以使用hash表来记录访问过的节点),这个标志只会给访问过的右节点的父节点进行标记,代码如下:

traverse_no_recursion_LRD (root) {var stack = [null]var p = rootwhile (stack.length > 0) {// 将所有左节点放入stack中while (p) {stack.push(p)p = p.left}var lastNode = stack[stack.length - 1]if (lastNode && !lastNode.visited && lastNode.right) {p = lastNode.right// 这里添加一个已经访问过右子树的标志,不加标志的话,会在对右子树的访问陷入无限循环中lastNode.visited = true} else {var node = stack.pop()console.log(node.val)}} }

层序遍历

层序遍历可以借助队列来实现,先将根节点放入队列中,然后进行出队列操作,判断出队列的节点是否有左右子节点,有的话,先左节点入队列,再右节点入队列,然后进行出队列操作,直到队列为空,由于队列先进先出的特点,左节点比右节点先进先出,那么左节点的子树也一样会比右节点的子树先进先出,也就是说,出队列的顺序就是层序遍历的顺序,代码如下:

traverse_level (root) {// 这里吧queue当成队列使用,入队列是push,出队列是shiftvar queue = []queue.push(root)while (queue.length > 0) {//出队列var root = queue.shift()// 输出rootconsole.log(root.val)// 有左右子节点的话,依次放入if (root.left) queue.push(root.left)if (root.right) queue.push(root.right)}}

上文也说了,层序遍历其实就是二叉树的广度优先搜索算法,所以很容易就可以类推出树的广度优先搜索算法,其实就是把左右子节点入队列换成将所有子节点入队列即可。同理,深度优先搜索算法和先序遍历其实是相同的。

二叉树的遍历十分重要,需要深刻理解并多加练习!

二叉树的递归与非递归遍历详解相关推荐

  1. 【C语言】二叉树前序中序后序遍历详解!!!内附算法好题初阶(每日小细节021)

    二叉树三种遍历方式时刻牢记,所谓的前中后就是根的位置 前序:根->左子树->右子树 中序:左子树->根->右子树 后序:左子树->右子树->根 每日小细节新增算法好 ...

  2. 二叉树前中后序遍历的非递归实现以及层次遍历、zig-zag型遍历详解

    前言 二叉树的遍历是一个比较常见的问题,递归实现二叉树的前中后序遍历比较简单,但非递归实现二叉树的前中后序遍历相对有难度.这篇博客将详述如何使用非递归的方式实现二叉树的前中后序遍历,在进行理论描述的同 ...

  3. 二叉树的非递归遍历和层次遍历详解

    二叉树非递归遍历非递归的后序遍历二叉树 //非递归的后续遍历二叉树 void HXprint(Tree *tree){Stack s = initStack(); //初始化一个下面使用的栈 tree ...

  4. 二叉树的几种递归和非递归式遍历:

    二叉树的几种递归和非递归式遍历: 1 #include <fstream> 2 #include <iostream> 3 4 using namespace std; 5 6 ...

  5. 分别用递归和非递归方式实现二叉树先序、中序和后序遍历(java实现)

    分别用递归和非递归方式实现二叉树先序.中序和后序遍历 用递归和非递归方式,分别按照二叉树先序.中序和后序打印所有的节点.我们约定:先序遍历顺序 为根.左.右;中序遍历顺序为左.根.右;后序遍历顺序为左 ...

  6. 转载:二叉树的前中后和层序遍历详细图解(递归和非递归写法)

    二叉树的前中后和层序遍历详细图解(递归和非递归写法) Monster_ii 2018-08-27 17:01:53 50530 收藏 403 分类专栏: 数据结构拾遗 文章标签: 二叉树 前序 中序 ...

  7. 九十五、二叉树的递归和非递归的遍历算法模板

    @Author:Runsen 刷Leetcode,需要知道一定的算法模板,本次先总结下二叉树的递归和非递归的遍历算法模板. 二叉树的四种遍历方式,前中后加上层序遍历.对于二叉树的前中后层序遍历,每种遍 ...

  8. 二叉树层序遍历递归与非递归_二叉树基础(1)-构建和遍历(递归和非递归)...

    二叉树的构建有2种方式:1.直接输入数字.2.根据两种顺序来判断另外一中顺序(后面会提到) 这里分享第一种构建方式,二叉树的前中后三种遍历方式(递归和非递归版本),和二叉树的层次遍历. 见代码demo ...

  9. 二叉树的遍历:先序 中序 后序遍历的递归与非递归实现及层序遍历

    二叉树的定义:一种基本的数据结构,是一种每个节点的儿子数目都不多于2的树 树节点的定义如下: // 树(节点)定义 struct TreeNode {int data; // 值TreeNode* l ...

  10. 左神算法:分别用递归和非递归方式实现二叉树先序、中序和后序遍历(Java版)

    本题来自左神<程序员代码面试指南>"分别用递归和非递归方式实现二叉树先序.中序和后序遍历"题目. 题目 用递归和非递归方式,分别按照二叉树先序.中序和后序打印所有的节点 ...

最新文章

  1. 多版本Python共存时pip给指定版本的python安装package的方法
  2. Sybase中字符串替换函数 STR REPLACE
  3. mysql表中插中文报错_向mysql表中插入含有中文的数据时报错:[Err] 1366
  4. 转:常用的几种加密算法以及java实现
  5. windows下vagrant的安装使用
  6. session mysql java_PHP自定义session处理方法,保存到MySQL数据库中
  7. 草稿 datagridview的显示与修改
  8. linux中搭建git私有服务器
  9. typedef用法(二)
  10. 自动控制原理思维导图
  11. 内存管理中的虚拟地址到物理地址翻译
  12. 花了我一个晚上时间整理的Python魂斗罗小游戏源代码
  13. C++基础课 5- 章
  14. 专变采集终端、配变采集终端、采集器、集中器
  15. 高仿微信抢红包动画特效
  16. 推荐一款好用的CopyTranslator 翻译工具
  17. 教程篇(6.4) 02. FortiOS架构 ❀ 企业防火墙 ❀ Fortinet 网络安全架构师 NSE7
  18. 液晶屏循环显示单色 液晶屏检测程序
  19. b500k电位器引脚接法_收录机旋转电位器b500k,可调式电位器25k
  20. 摩根士丹利分析报告:中国正在调整经济底层逻辑!

热门文章

  1. 没基础的大学生如何自学c语言 ?
  2. 如何开发自己的VUE组件库并打包复用
  3. WPS js宏 md5
  4. 被问到最多的淘口令API调用方法
  5. 2021图像检索综述
  6. 【radon变换原理讲解及利用python库函数快速实现】
  7. yum源提示出现Another app is currently holding the yum lock; waiting for it to exit...
  8. 项目管理过程4W1H
  9. PMBOK 49过程 ITTO和4W1H(第六版)
  10. 软件测试-5W1H原则