【leetcode热题】 二叉树的后序遍历

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 

示例 1:

输入:root = [1,null,2,3]
输出:[3,2,1]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

解法一 递归

和之前的中序遍历和先序遍历没什么大的改变,只需要改变一下 list.add 的位置。

public List<Integer> postorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();postorderTraversalHelper(root, list);return list;
}private void postorderTraversalHelper(TreeNode root, List<Integer> list) {if (root == null) {return;}postorderTraversalHelper(root.left, list);postorderTraversalHelper(root.right, list);list.add(root.val);
}

解法二 栈

主要就是用栈要模拟递归的过程,区别于之前 94 题 中序遍历和 144 题 先序遍历,后序遍历的非递归形式会相对难一些。

原因就是,当遍历完某个根节点的左子树,回到根节点的时候,对于中序遍历和先序遍历可以把当前根节点从栈里弹出,然后转到右子树。举个例子,

     1/ \2   3/ \4   5

当遍历完 2,4,5 的时候,回到 1 之后我们就可以把 1 弹出,然后通过 1 到达右子树继续遍历。

而对于后序遍历,当我们到达 1 的时候并不能立刻把 1 弹出,因为遍历完右子树,我们还需要将这个根节点加入到 list 中。

所以我们就需要判断是从左子树到的根节点,还是右子树到的根节点。

如果是从左子树到的根节点,此时应该转到右子树。如果是从右子树到的根节点,那么就可以把当前节点弹出,并且加入到 list 中。

当然,如果是从左子树到的根节点,此时如果根节点的右子树为 null, 此时也可以把当前节点弹出,并且加入到 list 中。

基于上边的思想,可以写出一些不同的代码。

1. 思想一

可以先看一下中序遍历的实现。

public List<Integer> inorderTraversal(TreeNode root) {List<Integer> ans = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.isEmpty()) {//节点不为空一直压栈while (cur != null) {stack.push(cur);cur = cur.left; //考虑左子树}//节点为空,就出栈cur = stack.pop();//当前值加入ans.add(cur.val);//考虑右子树cur = cur.right;}return ans;
}

这里后序遍历的话,和中序遍历有些像。

开始的话,也是不停的往左子树走,然后直到为 null 。不同之处是,之前直接把节点 pop 并且加入到 list 中,然后直接转到右子树。

这里的话,我们应该把节点 peek 出来,然后判断一下当前根节点的右子树是否为空或者是否是从右子树回到的根节点。

判断是否是从右子树回到的根节点,这里我用了一个 set ,当从左子树到根节点的时候,我把根节点加入到 set 中,之后我们就可以判断当前节点在不在 set 中,如果在的话就表明当前是第二次回来,也就意味着是从右子树到的根节点。

public List<Integer> postorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();Set<TreeNode> set = new HashSet<>();TreeNode cur = root;while (cur != null || !stack.isEmpty()) {while (cur != null && !set.contains(cur)) {stack.push(cur);cur = cur.left;}cur = stack.peek();//右子树为空或者第二次来到这里if (cur.right == null || set.contains(cur)) {list.add(cur.val);set.add(cur);stack.pop();//将当前节点弹出if (stack.isEmpty()) {return list;}//转到右子树,这种情况对应于右子树为空的情况cur = stack.peek();cur = cur.right;//从左子树过来,加到 set 中,转到右子树} else {set.add(cur);cur = cur.right;}}return list;
}

上边的代码把一些情况其实做了合并,并不是很好理解,下边分享一下 solution 里的一些简洁的解法。

2. 思想二

上边的解法在判断当前是从左子树到的根节点还是右子树到的根节点用了 set ,这里 还有一个更直接的方法,通过记录上一次遍历的节点。

如果当前节点的右节点和上一次遍历的节点相同,那就表明当前是从右节点过来的了。

public List<Integer> postorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;TreeNode last = null;while (cur != null || !stack.isEmpty()) {if (cur != null) {stack.push(cur);cur = cur.left;} else {TreeNode temp = stack.peek();//是否变到右子树if (temp.right != null && temp.right != last) {cur = temp.right;} else {list.add(temp.val);last = temp;stack.pop();}}}return list;
}

3. 思想三

在 这里 看到另一种想法,还是基于上边分析的入口点,不过解决方案真的是太优雅了。

先看一下 144 题 前序遍历的代码。

我们还可以将左右子树分别压栈,然后每次从栈里取元素。需要注意的是,因为我们应该先访问左子树,而栈的话是先进后出,所以我们压栈先压右子树。

public List<Integer> preorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();if (root == null) {return list;}Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()) {TreeNode cur = stack.pop();if (cur == null) {continue;}list.add(cur.val);stack.push(cur.right);stack.push(cur.left);}return list;
}

后序遍历遇到的问题就是到根节点的时候不能直接 pop ,因为后边还需要回来。

上边的作者,提出只需要把每个节点 push 两次,然后判断当前 pop 节点和栈顶节点是否相同。

相同的话,就意味着是从左子树到的根节点。

不同的话,就意味着是从右子树到的根节点,此时就可以把节点加入到 list 中。

public List<Integer> postorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();if (root == null) {return list;}Stack<TreeNode> stack = new Stack<>();stack.push(root);stack.push(root);while (!stack.isEmpty()) {TreeNode cur = stack.pop();if (cur == null) {continue;}if (!stack.isEmpty() && cur == stack.peek()) {stack.push(cur.right);stack.push(cur.right);stack.push(cur.left);stack.push(cur.left);} else {list.add(cur.val);}}return list;
}

解法三 转换问题

首先我们知道前序遍历的非递归形式会比后序遍历好理解些,那么我们能实现后序遍历 -> 前序遍历的转换吗?

后序遍历的顺序是 左 -> 右 -> 根

前序遍历的顺序是 根 -> 左 -> 右,左右其实是等价的,所以我们也可以轻松的写出 根 -> 右 -> 左 的代码。

然后把 根 -> 右 -> 左 逆序,就是 左 -> 右 -> 根,也就是后序遍历了。

让我们改一下之前 144 题 先序遍历的代码。

改之前的代码。

public List<Integer> preorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.isEmpty()) {if (cur != null) {list.add(cur.val);stack.push(cur);cur = cur.left; //考虑左子树}else {//节点为空,就出栈cur = stack.pop();//考虑右子树cur = cur.right;}}return list;
}

然后我们只需要把上边的 left 改成 rightright 改成 left 就可以了。最后倒置即可。

public List<Integer> postorderTraversal2(TreeNode root) {List<Integer> list = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.isEmpty()) {if (cur != null) {list.add(cur.val);stack.push(cur);cur = cur.right; // 考虑左子树} else {// 节点为空,就出栈cur = stack.pop();// 考虑右子树cur = cur.left;}}Collections.reverse(list);return list;
}

同样的,之前先序遍历的 Morris Traversal ,不需要额外空间的解法。

public List<Integer> preorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();TreeNode cur = root;while (cur != null) {//情况 1if (cur.left == null) {list.add(cur.val);cur = cur.right;} else {//找左子树最右边的节点TreeNode pre = cur.left;while (pre.right != null && pre.right != cur) {pre = pre.right;}//情况 2.1if (pre.right == null) {list.add(cur.val);pre.right = cur;cur = cur.left;}//情况 2.2if (pre.right == cur) {pre.right = null; //这里可以恢复为 nullcur = cur.right;}}}return list;
}

同样的处理,把上边的 left 改成 rightright 改成 left 就可以了。最后倒置即可。

public List<Integer> postorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();TreeNode cur = root;while (cur != null) {if (cur.right == null) {list.add(cur.val);cur = cur.left;} else {TreeNode pre = cur.right;while (pre.left != null && pre.left != cur) {pre = pre.left;}if (pre.left == null) {list.add(cur.val);pre.left = cur;cur = cur.right;}if (pre.left == cur) {pre.left = null; // 这里可以恢复为 nullcur = cur.left;}}}Collections.reverse(list);return list;
}

上边的话由于我们用的是 ArrayList ,所以倒置的话其实是比较麻烦的,可能需要更多的时间或空间。

所以我们可以用 LinkedList , 这样倒置链表就只需要遍历一遍,也不需要额外的空间了。

更近一步,我们在调用 list.add 的时候,其实可以直接 list.addFirst ,每次都插入到链表头,这样做的话,最后也不需要逆转链表了。

解法四 Morris Traversal

上边已经成功改写 Morris Traversal 了,但是是一种取巧的方式,通过变形的前序遍历做的。同学介绍了另一种写法,这里也分享下。

94 题 中序遍历中对 Morris 遍历有详细的介绍,我先贴过来。

我们知道,左子树最后遍历的节点一定是一个叶子节点,它的左右孩子都是 null,我们把它右孩子指向当前根节点,这样的话我们就不需要额外空间了。这样做,遍历完当前左子树,就可以回到根节点了。

当然如果当前根节点左子树为空,那么我们只需要保存根节点的值,然后考虑右子树即可。

所以总体思想就是:记当前遍历的节点为 cur

1、cur.left 为 null,保存 cur 的值,更新 cur = cur.right

2、cur.left 不为 null,找到 cur.left 这颗子树最右边的节点记做 last

2.1 last.right 为 null,那么将 last.right = cur,更新 cur = cur.left

2.2 last.right 不为 null,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 cur 的值,更新 cur = cur.right

结合图示:

如上图,cur 指向根节点。 当前属于 2.1 的情况,cur.left 不为 nullcur 的左子树最右边的节点的右孩子为 null,那么我们把最右边的节点的右孩子指向 cur

接着,更新 cur = cur.left

如上图,当前属于 2.1 的情况,cur.left 不为 null,cur 的左子树最右边的节点的右孩子为 null,那么我们把最右边的节点的右孩子指向 cur

更新 cur = cur.left

如上图,当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right

如上图,当前属于 2.2 的情况,cur.left 不为 nullcur 的左子树最右边的节点的右孩子已经指向 cur,保存 cur 的值,更新 cur = cur.right

如上图,当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right

如上图,当前属于 2.2 的情况,cur.left 不为 nullcur 的左子树最右边的节点的右孩子已经指向 cur,保存 cur 的值,更新 cur = cur.right

当前属于情况 1,cur.left 为 null,保存 cur 的值,更新 cur = cur.right

cur 指向 null,结束遍历。

根据这个关系,写代码

记当前遍历的节点为 cur

1、cur.left 为 null,保存 cur 的值,更新 cur = cur.right

2、cur.left 不为 null,找到 cur.left 这颗子树最右边的节点记做 last

2.1 last.right 为 null,那么将 last.right = cur,更新 cur = cur.left

2.2 last.right 不为 null,说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 cur 的值,更新 cur = cur.right

public List<Integer> inorderTraversal(TreeNode root) {List<Integer> ans = new ArrayList<>();TreeNode cur = root;while (cur != null) {//情况 1if (cur.left == null) {ans.add(cur.val);cur = cur.right;} else {//找左子树最右边的节点TreeNode pre = cur.left;while (pre.right != null && pre.right != cur) {pre = pre.right;}//情况 2.1if (pre.right == null) {pre.right = cur;cur = cur.left;}//情况 2.2if (pre.right == cur) {pre.right = null; //这里可以恢复为 nullans.add(cur.val);cur = cur.right;}}}return ans;
}

根据上边的关系,我们会发现除了叶子节点只访问一次,其他节点都会访问两次,结合下图。

当第二次访问某个节点的时候,我们只需要将它的左节点,以及左节点的右节点,左节点的右节点的右节点... 逆序添加到 list 中即可。比如上边的例子。

上边的遍历顺序其实就是按照深度优先的方式。

先访问 15, 7, 3, 1 然后往回走
3 第二次访问,将它的左节点逆序加入到 list 中
list = [1]继续访问 2, 然后往回走
7 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中
list = [1 2 3]继续访问 6 4, 然后往回走
6 第二次访问, 将它的左节点逆序加入到 list 中
list = [1 2 3 4]继续访问 5, 然后往回走
15 第二次访问, 将它的左节点, 左节点的右节点, 左节点的右节点的右节点逆序加入到 list 中
list = [1 2 3 4 5 6 7]然后访问 14 10 8, 然后往回走
10 第二次访问,将它的左节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8]继续访问 9, 然后往回走
14 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8 9 10]继续访问 13 11, 然后往回走
13 第二次访问, 将它的左节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8 9 10 11]继续遍历 12,结束遍历然后单独把根节点,以及根节点的右节点,右节点的右节点,右节点的右节点的右节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]得到 list 就刚好是后序遍历

如下图,问题就转换成了 9 组单链表的逆序问题。

当遇到第二次访问的节点,我们将单链表逆序,然后加入到 list 并且还原即可。单链表逆序已经在 第 2 题 讨论过了,直接拿过来用,只需要把 node.next 改为 node.right 。

public List<Integer> postorderTraversal(TreeNode root) {List<Integer> list = new ArrayList<>();TreeNode cur = root;while (cur != null) {// 情况 1if (cur.left == null) {cur = cur.right;} else {// 找左子树最右边的节点TreeNode pre = cur.left;while (pre.right != null && pre.right != cur) {pre = pre.right;}// 情况 2.1if (pre.right == null) {pre.right = cur;cur = cur.left;}// 情况 2.2,第二次遍历节点if (pre.right == cur) {pre.right = null; // 这里可以恢复为 null//逆序TreeNode head = reversList(cur.left);//加入到 list 中,并且把逆序还原reversList(head, list);cur = cur.right;}}}TreeNode head = reversList(root);reversList(head, list);return list;
}private TreeNode reversList(TreeNode head) {if (head == null) {return null;}TreeNode tail = head;head = head.right;tail.right = null;while (head != null) {TreeNode temp = head.right;head.right = tail;tail = head;head = temp;}return tail;
}private TreeNode reversList(TreeNode head, List<Integer> list) {if (head == null) {return null;}TreeNode tail = head;head = head.right;list.add(tail.val);tail.right = null;while (head != null) {TreeNode temp = head.right;head.right = tail;tail = head;list.add(tail.val);head = temp;}return tail;
}

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

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

相关文章

读算法的陷阱:超级平台、算法垄断与场景欺骗笔记05_共谋(中)

1. 默许共谋 1.1. 又称寡头价格协调&#xff08;Oligopolistic Price Coordination&#xff09;或有意识的平行行为&#xff08;Conscious Parallelism&#xff09; 1.1.1. 在条件允许的情况下&#xff0c;它会发生在市场集中度较高的行业当中 1.1.…

蓝桥杯递推与递归法|斐波那契数列|数字三角形|42点问题|数的计算|数的划分(C++)

递归是用来做dfs&#xff0c;是搜索算法的基础 递推是用来做dp部分&#xff0c;及部分其他算法&#xff0c;复杂度较低&#xff0c;不会出现爆栈问题递推法&#xff1a; 递推法是一种在数学和其他领域广泛应用的重要方法&#xff0c;它在计算机科学中被用作一种关键的数值求解…

构建高效可靠的消息队列系统:设计与实现

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; 目录 一、引言 二、设计目标 2.1、高可用性 1. 集群搭建 1.1 …

二 超级数据查看器   讲解稿   导入功能

二 超级数据查看器 讲解稿 导入功能 APP下载地址 百度手机助手 下载地址4 ​ 讲解稿全文&#xff1a; 大家好。 今天我们对 超级数据查看器的 导入信息功能 做一下详细讲解。 首先&#xff0c;我们打开 超级数据查看器。 我们这个系统要实现的是&#xff0c;快速生…

qt 日志 格式化打印 QMessagePattern

进入 qt源码 调试:qt creator debug 无法进入 qt源码 调试-CSDN博客 qt为 格式化打印 日志 提供了一个简易的 pattern(模式/格式) 词法解析类QMessagePattern,该类在qt的专门精心日志操作的源码文件Src\qtbase\src\corelib\global\qlogging.cpp 中 该类直接在构造函数中…

C++笔记之嵌套类中的成员函数识别外层类的成员变量

C++笔记之嵌套类中的成员函数识别外层类的成员变量 —— 杭州 2024-03-10 code review! 文章目录 C++笔记之嵌套类中的成员函数识别外层类的成员变量1.嵌套类声明完之后跟一个标识符是什么含义?2.嵌套类中的成员函数如何识别外层类的成员变量?1.嵌套类声明完之后跟一个标识…

在Blender中清理由Instant-NGP等几何学习技术生成的网格

使用布尔运算: 创建一个大的立方体或其他简单几何体包裹住全部网格。使用布尔修改器对两个网格进行“差集”运算。这将移除超出包裹体之外的多余网格部分。 手动选择并删除: 进入编辑模式&#xff08;按Tab键&#xff09;。按A键取消选择所有顶点。按B键并拖动以选择您想要删除…

VBA更新xlOLELinks链接的值

xlOLELinks是在Excel文档中插入对象的链接&#xff0c;该链接能够显示被插入文档的数据&#xff0c;通常情况下链接的数值会自动更新&#xff0c;但有时更新也会不及时或失效&#xff0c;这时就需要手动更新&#xff0c;如下图&#xff1a; 以插入Word文档为例&#xff0c;使用…

【C语言】文件操作篇-----程序文件和数据文件,文件的打开和关闭,二进制文件和文本文件,fopen,fclose【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为【C语言】文件操作篇-----程序文件和数据文件&#xff0c;文件的打开和关闭&#xff0c;二进制文件和文本文件【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 在了解完动态内存管…

KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记10 - STM32的SDIO学习2

KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记10 - STM32的SDIO学习2 一、问题回顾二、本次的任务三、 需要注意的问题3.1 Card Identification Mode时的时钟频率3.2 CMD0指令的疑似问题3.3 发送带参数的ACMD41时要注意时间时序和时效3.4 CPSM的指令发送问题3.5 调试过程中的SD卡的…

LC打怪录 数组array

数组&#xff08;Array&#xff09; definition: 一种线性表数据结构。它使用一组连续的内存空间&#xff0c;来存储一组具有相同类型的数据。 如上图所示&#xff0c;假设数据元素的个数为 nnn&#xff0c;则数组中的每一个数据元素都有自己的下标索引&#xff0c;下标索引从…

线上应用部署了两台load为1四核服务器

线上应用部署了两台服务器。 项目发布后&#xff0c;我对线上服务器的性能进行了跟踪&#xff0c;发现一台负载为3&#xff0c;另一台负载为1&#xff0c;其中一台四核服务器已经快到瓶颈了&#xff0c;所以我们紧急排查原因。 1、使用TOP命令查看占用CPU较大的负载和进程&…