代码随想录算法训练营day14||二叉树part01、理论基础、递归遍历、迭代遍历、统一迭代

递归遍历 (必须掌握)

本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。

这里帮助大家确定下来递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

好了,我们确认了递归的三要素,接下来就来练练手:

以下以前序遍历为例:

  1. 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入List来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
     public void preorder(TreeNode root, List<Integer> result) 
  2. 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
  if (root == null){return; 
}

       3.确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中间节点的数值,代码如下:

        result.add(root.val); //中preorder(root.left, result); //左preorder(root.right, result); //右

单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,再看一下完整代码:

class Solution {//前序遍历·递归·LC144_二叉树的前序遍历public List<Integer> preorderTraversal(TreeNode root) {
//定义了一个名为preorderTraversal 的公共方法,该方法接受一个TreeNode类型的参数root,表示二叉树的根节点。
//该方法的返回类型是List<Integer>,表示二叉树的前序遍历结果。List<Integer> result = new ArrayList<Integer>();//创建一个名为result的ArrayList对象,用于存储遍历结果preorder(root,result);//调用preorder方法进行前序遍历,传入根节点root和结果列表result。return result;//返回遍历结果。}public void preorder(TreeNode root,List<Integer> result){
//定义了一个名为preorder的方法,用于执行前序遍历。
//该方法接受一个TreeNode类型的参数root,表示当前遍历的节点,以及一个List<Integer>类型的参数result,用于存储遍历结果。if(root == null){ //如果当前节点root为空,则直接返回,结束当前递归。return;}result.add(root.val);//中;将当前节点的值添加到结果列表中,表示当前节点的访问顺序是在中间。preorder(root.left,result); //递归地对左子树和右子树进行前序遍历。preorder(root.right,result); }
}

同day04中总结的递归法一样:

递归法的宗旨就是紧紧抓住原来的函数究竟返回的是什么?作用是什么? 即可

其余的细枝末节不要细究,编译器会帮我们自动完成。

递归函数preorder()需要传入TreeNode类型的节点和一个List<Integer>类型的参数result用来存储遍历结果,调用递归 直接把相关的参数填进去即可。

 preorder(root.left,result); //递归地对左子树和右子树进行前序遍历。
 preorder(root.right,result); 

class Solution {//中序遍历·递归·LC145_二叉树的后序遍历public List<Integer> postorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<Integer>();posorder(root,result);return result;}public void posorder(TreeNode root,  List<Integer> result){if(root == null){return;}posorder(root.left,result); //左posorder(root.right,result); //右result.add(root.val);  //中}
}
class Solution {public List<Integer> inorderTraversal(TreeNode root) {//中序遍历·递归·LC94_二叉树的中序遍历List<Integer> result = new ArrayList<Integer>();inorder(root,result);return result;}public void inorder(TreeNode root,  List<Integer> result){if(root == null){return;}inorder(root.left,result); //左result.add(root.val);  //中inorder(root.right,result); //右}
}

 迭代遍历 (基础不好的录友,迭代法可以放过)

思路

为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢?

我们在栈与队列:匹配问题都是栈的强项 (opens new window)中提到了,递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

此时大家应该知道我们用栈也可以是实现二叉树的前后中序遍历了。

前序遍历(迭代法)

我们先看一下前序遍历。

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。

为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。

此时会发现貌似使用迭代法写出前序遍历并不难,确实不难。

此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?其实还真不行!

但接下来,再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。

class Solution {//前序遍历顺序:中-左-右,入栈顺序:中-右-左public List<Integer> preorderTraversal(TreeNode root) {
//定义了一个名为preorderTraversal 的公共方法,该方法接受一个TreeNode 类型的参数root,表示二叉树的根节点。
//该方法的返回类型是List<Integer>,表示二叉树的前序遍历结果。List<Integer> result = new ArrayList<Integer>();//创建一个名为 result 的 ArrayList 对象,用于存储遍历结果。//然后检查根节点是否为空,如果为空,则直接返回空结果列表。if(root == null){return result;}Stack<TreeNode> stack = new Stack<>();//创建一个名为stack的栈,用于存储待访问的节点。并将根节点root入栈。stack.push(root);while(!stack.isEmpty()){//使用循环来遍历栈中的节点,直到栈为空为止。TreeNode node = stack.pop();//从栈中弹出一个节点,表示当前要访问的节点。result.add(node.val);//将当前节点的值添加到结果列表中,表示当前节点的访问顺序是在中间。//将当前节点的右子节点和左子节点依次入栈。//由于栈的特性,右子节点先入栈,然后左子节点入栈,这样在下一次循环时先访问左子节点,符合前序遍历的顺序。if(node.right != null){stack.push(node.right);}if(node.left != null){stack.push(node.left);//然后继续执行while{}中的内容,使用循环来遍历栈中的节点,直到栈为空为止} }return result;//最后返回结果列表result,其中存储了二叉树的前序遍历结果。}
}

有了前序遍历,先讨论后序遍历(迭代法),比较容易:

再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:

所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下: 

// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {public List<Integer> postorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null){return result;}Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()){TreeNode node = stack.pop();result.add(node.val);if (node.left != null){stack.push(node.left);}if (node.right != null){stack.push(node.right);}}Collections.reverse(result);return result;}
}

中序遍历(迭代法):(这个过程需要多理解以下,第一遍有点绕没看懂)

为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:

  1. 处理:将元素放进result数组中
  2. 访问:遍历节点

分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null){return result;}//创建一个名为stack的栈,用于存储待访问的节点。//同时创建一个名为cur的节点,初始值为根节点root,用于迭代遍历二叉树。Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;//使用循环来遍历二叉树的所有节点,直到当前节点为空 且 栈为空为止。while (cur != null || !stack.isEmpty()){//如果当前节点cur不为空,则将当前节点入栈,并将当前节点移动到其左子节点。if (cur != null){stack.push(cur);cur = cur.left;}else{//如果当前节点为空,则从栈中弹出一个节点,表示当前要访问的节点,//将该节点的值添加到结果列表中,然后将当前节点移动到其右子节点。cur = stack.pop();result.add(cur.val);cur = cur.right;}}return result;//最后返回结果列表result,其中存储了二叉树的中序遍历结果。}
}

 

统一迭代(基础不好的录友,迭代法可以放过)

这是统一迭代法的写法, 如果学有余力,可以掌握一下

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

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

相关文章

2024年最新IntelliJ IDEA:新特性与免费激活技巧

大家好&#xff0c;我是小黑&#xff0c;在如今这个信息化的时代&#xff0c;软件工程师们势必需要一款极致高效且功能全面的开发工具来提升他们的编码体验。在众多的IDE&#xff08;集成开发环境&#xff09;中&#xff0c;IntelliJ IDEA无疑以其强大的功能和易用性&#xff0…

解密输入输出迷局:蓝桥杯与ACM中C++/C语言常见问题揭秘

关于C中的常见输入输出汇总 带空格的字符串&#xff1a; ​ 对于这种输入方式我们选择使用gets() 函数来进行输入&#xff0c;gets用于从标准输入&#xff08;通常是键盘&#xff09;读取一行文本并将其存储为字符串&#xff0c;直到遇到换行符&#xff08;‘\n’&#xff09…

Ondo宣布将其原生稳定币USDY带入Sui生态

重要提示&#xff1a;USDY是由短期美国国债支持的token化票据&#xff0c;持有者享受稳定币的实用性同时获得收益。USDY不得在美国或向美国人出售或以其他方式提供。USDY也未根据1933年美国证券法注册。 不到一年的时间&#xff0c;Sui已经成为全链TVL排名前十的区块链&#xf…

使用 WPF + Chrome 内核实现高稳定性的在线客服系统复合应用程序

对于在线客服与营销系统&#xff0c;客服端指的是后台提供服务的客服或营销人员&#xff0c;他们使用客服程序在后台观察网站的被访情况&#xff0c;开展营销活动或提供客户服务。在本篇文章中&#xff0c;我将详细介绍如何通过 WPF Chrome 内核的方式实现复合客服端应用程序。…

360 安全浏览器 - 页面布局 - 常用网址

360 安全浏览器 - 页面布局 - 常用网址 自定义样式 let myStyle {https://www.baidu.com/: {color: #001483,backgroundColor: #FFF,icon: https://www.baidu.com/favicon.ico},https://blog.csdn.net/jx520: {backgroundColor: #fc5531,icon: https://g.csdnimg.cn/static/l…

软件测试工程师——缺陷(一篇足以)

目录 定义 缺陷的类型 缺陷的严重程度 缺陷的状态 缺陷的根源 ​缺陷的来源 缺陷的起源 缺陷的生命周期 缺陷的识别 缺陷报告模板 编写缺陷报告的目的 缺陷报告编写的准则 缺陷描述的准则 定义 1. 软件未实现产品说明书中所提及的功能 2. 软件实现了产品说明书中…

嵌入式学习之Linux入门篇笔记——9,Linux权限管理

配套视频学习链接&#xff1a;http://【【北京迅为】嵌入式学习之Linux入门篇】 https://www.bilibili.com/video/BV1M7411m7wT/?p4&share_sourcecopy_web&vd_sourcea0ef2c4953d33a9260910aaea45eaec8 1.为什么要管理 Linux 权限&#xff1f; 很好管理每个用户。控制每…

Redis篇之redis是单线程

一、redis是单线程 Redis是单线程的&#xff0c;但是为什么还那么快&#xff1f;主要原因有下面3点原因&#xff1a; 1. Redis是纯内存操作&#xff0c;执行速度非常快。 2. 采用单线程&#xff0c;避免不必要的上下文切换可竞争条件&#xff0c;多线程还要考虑线程安全问题。 …

【C++】【类和对象】拷贝构造函数

1.拷贝构造函数的特性&#xff1a; 1.拷贝构造函数用来构造一个与已存在对象一摸一样的对象 它只有单个形参&#xff0c;该形参是对本类类型对象的引用(一般常用const修饰)&#xff0c;在用已存在的类类型对象创建新对象时由编译器自动调用。 2.拷贝构造函数是构造函数的一种重…

报错ValueError: Unknown CUDA arch (8.6) or GPU not supported

文章目录 问题描述解决方案参考文献 问题描述 报错 ValueError: Unknown CUDA arch (8.6) or GPU not supported 本人显卡为 RTX 3060&#xff0c;CUDA 为 10.2&#xff0c;PyTorch 为 1.5 解决方案 修改 C:\Users\Administrator\Envs\test\Lib\site-packages\torch\utils\c…

FATFA文件系统

一.文件系统基本知识 1.文件系统是什么&#xff1f; 文件系统是一种用于组织和存储计算机上的文件和目录的方法。它是操作系统中的一个重要组成部分&#xff0c;负责管理磁盘或其他存储介质上的文件&#xff0c;使其易于访问和使用。文件系统提供了一种结构化的方式来组织文件…

JavaScript相关(一)——作用域

本篇将从JS的执行上下文开始&#xff0c;去理解&#xff1a;变量提升、 栈式调用、作用域和闭包。 参考&#xff1a; 浏览器工作原理与实践 JS执行上下文 执行上下文是 JavaScript 执行一段代码时的运行环境&#xff0c;比如调用一个函数&#xff0c;就会生成这个函数的执行…