用递归的思想实现二叉树前、中、后序迭代遍历

简介: 用递归的思想实现二叉树前、中、后序迭代遍历

先复习一下前、中、后遍历的顺序:

  1. 前序遍历顺序:中-左-右
  2. 中序遍历顺序:左-中-右
  3. 后序遍历顺序:左-右-中

用递归来写二叉树遍历是非常简单的,例如前序遍历的代码如下:

const result = []
function preorderTraversal(node) {
    if (!node) return null
    result.push(node.val)
    preorderTraversal(node.left)
    preorderTraversal(node.right)
}
preorderTraversal(root)

我们都知道,在调用函数时,系统会在栈中为每个函数维护相应的变量(参数、局部变量、返回地址等等)。

例如有 a,b,c 三个函数,先调用 a,a 又调用 b,b 最后调用 c。此时的调用栈如图所示:

为什么要说这个呢?因为递归遍历的执行过程就是这样的,只不过是函数不停的调用自身,直到遇到递归出口(终止条件)。

举个例子,现在要用递归前序遍历以下二叉树:

1
    \
     2
    /
   3

它的遍历顺序为 1-2-3,调用栈如图所示:

理解了递归调用栈的情况,再来看看怎么利用递归思想实现前序迭代遍历:

function preorderTraversal(root) {
    const result = []
    // 用一个数组 stack 模拟调用栈
    const stack = []
    let node = root
    while (stack.length || node) {
        // 递归遍历节点的左子树,直到空为止
        while (node) {
            result.push(node.val)
            stack.push(node)
            node = node.left
        }
        // 跳出循环时 node 为空,由于前序遍历的特性
        // 当前 node 节点的上一个节点必定是它的父亲节点
        // 前序遍历是中-左-右,现在左子树已经到头了,该遍历父节点的右子树了
        // 所以要弹出父节点,从它的右子树开始新一轮循环
        node = stack.pop()
        node = node.right
    }
    return result
}

再看一个具体的示例,用迭代遍历跑一遍:

1
          /    \
         2      3
        /  \   /  \
       4    5 6    7
  1. 初始节点 node 为 1
  2. while 遍历完它的左子节点
  3. 当前 stack == [1,2,4]
  4. 初始节点已经遍历完它下面的最后一个左子节点了,即节点 4 的左子节点,所以现在要开始遍历 4 的右子节点
  5. 弹出节点 4 并从它的右子节点开始新的循环
  6. 由于节点 4 的右子节点为空,所以不会进入 while 循环,然后弹出节点 4 的父节点 2
  7. 再从节点 2 的右子节点开始循环

看到这是不是已经发现了这个迭代遍历的过程和递归遍历的过程一模一样?

而且用递归的思想来实现迭代遍历,优点在于好理解,以后再遇到这种问题马上就能想起来怎么做了。

中序遍历

中序遍历和前序遍历差不多,区别在于节点出栈时,才将节点的值推入到 result 中。

function inorderTraversal(root) {
    const result = []
    const stack = []
    let node = root
    while (stack.length || node) {
        while (node) {
            stack.push(node)
            node = node.left
        }
        node = stack.pop()
        result.push(node.val)
        node = node.right
    }
    return result
}

后序遍历

前序遍历过程为中-左-右,逆前序遍历过程就是将遍历左右子树的顺序调换一下,即中-右-左。

然后再看一下后序遍历的过程左-右-中,可以看出逆前序遍历顺序的倒序就是后序遍历的顺序。

利用这一特点写出的后序遍历代码如下:

function postorderTraversal(root) {
    const result = []
    const stack = []
    let node = root
    while (stack.length || node) {
        while (node) {
            result.push(node.val)
            stack.push(node)
            node = node.right // 原来是 node.left,这里换成 node.right
        }
        node = stack.pop()
        node = node.left // 原来是 node.right,这里换成 node.left
    }
    return result.reverse() // 逆前序遍历顺序的倒序就是后序遍历的顺序
}

参考资料

他来了,他带着他的三兄弟来了,前中后序遍历统一的算法

目录
相关文章
|
Docker 容器
Minio Docker安装官方指南
Minio Docker安装官方指南
Minio Docker安装官方指南
|
缓存 JavaScript 前端开发
前端框架与库 - Vue.js基础:模板语法、数据绑定
【7月更文挑战第14天】Vue.js 是渐进式框架,以简洁API和高效数据绑定知名。本文聚焦模板语法与数据绑定,解释常见问题和易错点,助力初学者避坑。模板语法中,{{ expression }} 用于渲染值,v-bind/: 用于动态绑定属性。数据绑定涉及文本、属性和事件,注意v-model适用于表单元素,计算属性有缓存。理解正确用法,借助文档和IDE,可提升开发质量和效率。善用Vue.js,打造响应式UI。
515 4
|
11月前
|
运维 负载均衡 Linux
阿里云轻量服务器最新收费标准与价格参考
阿里云轻量服务器具有灵活的镜像选择、快速上手、简便运维等优势,轻量服务器适合个人开发者和学生用来搭建网站、云端学习等场景使用,2024年截至目前国内地域有60元/月、80元/月等套餐可选,国外地域有24元/月、34元/月、67元/月等套餐可选,目前轻量应用服务器2核2G3M带宽82元1年、2核4G4M带宽298元1年。
|
存储 Java Go
Go 语言切片如何扩容?(全面解析原理和过程)
Go 语言切片如何扩容?(全面解析原理和过程)
448 2
|
存储 数据库连接 数据安全/隐私保护
使用Python和Flask构建一个简单的Web博客应用
使用Python和Flask构建一个简单的Web博客应用
172 0
|
10月前
|
Python
探索编程之道:从代码中寻找生活的哲理
【10月更文挑战第35天】在编程的世界里,每一行代码都蕴含着深刻的意义。就像生活中的每一个选择都会影响我们的未来一样,代码中的每个决策也会塑造程序的运行结果。本文将通过一个简单的代码示例,探讨如何从编程中汲取生活的智慧,以及如何在面对技术挑战时保持初心和持续学习的态度。让我们一起走进编程的世界,发现那些隐藏在代码背后的生活哲理吧!
|
XML JSON 前端开发
前端 JS 经典:Content-type 详解
前端 JS 经典:Content-type 详解
695 0
|
存储 Java 测试技术
滚雪球学Java(09-5):Java中的赋值运算符,你真的掌握了吗?
【2月更文挑战第7天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!!
229 2
|
Rust Linux C语言
Rust让科学计算速度提升200倍
Rust 语言自身相对已经成熟,生态也足够丰富,并且在很多应用领域崭露头角。但是Rust陡峭的学习曲线让Rust目前依然是小众语言,缺乏成熟的开发者基础,这是CTO在引入Rust到技术栈时要考虑的重要问题。如果团队人才密度足够高,可以比较轻松地转到Rust,否则将会面临市场人才紧缺,能力参差不齐等问题,最终导致技术选型灾难。
539 0
Rust让科学计算速度提升200倍
|
存储 Java 程序员
强制类型转换运算符的深入解析
在编程中,类型转换是一个常见的操作,它允许我们将一个数据类型转换为另一个数据类型。在某些情况下,编译器可以自动执行这种转换,称为隐式类型转换。但在其他情况下,需要程序员显式地指定转换,这就是所谓的强制类型转换。
109 0