LeetCode 94. 二叉树的中序遍历个人主页 我不会起名字322 欢迎各位大佬莅临其他栏目 技术栈学习笔记 其他栏目 力扣Hot100题目解析 其他栏目 Go项目学习笔记 前言前面的回溯系列里我们一直在强调三要素——路径、选择列表、结束条件。但算法题不是只有回溯一种套路。从今天开始我们换一条线专门聊二叉树。二叉树题有个特点代码极短但递归逻辑极其密集。一道题可能只有五六行但每一行都在做递归调用如果对递归到底在干什么没有直觉看代码就像看天书。所以这个系列的第一篇我们不急着刷难题先把遍历这个最基础、也最核心的骨架吃透。中序遍历是理解二叉树递归的入口——把它想清楚了前序、后序只是换个位置的事后面的层序遍历、路径求和、最近公共祖先也都是在这个骨架上长出来的。首先我们说二叉树的递归到底是干了什么事递归是一种分而治之的策略当你把一棵树拆成根 左子树 右子树并且发现左子树和右子树又是同样结构的树时递归就自然出现了。遍历题用递归访问每个节点并在访问时收集答案。这道题稍微特殊一点——我们不是在每个节点处都做同一件事而是在左子树和右子树之间插入访问根的动作这个插入位置的不同直接决定了是前序、中序还是后序。递归的终止条件这是二叉树递归要看的第一个数据。这道题的终止条件就是当前节点为空。空节点不是没有节点而是递归的地基——没有它递归就会无限往下走。所以每个递归函数的第一行几乎都是if node nil { return }。当前节点要做什么这是二叉树递归要看的第二个数据。中序遍历里当前节点要做的事就是把它的值追加到结果集。但注意这个动作不是随便做的——它必须发生在左子树递归回来之后、右子树递归开始之前。这个位置就是中序的序。递归的去向这是二叉树递归要看的第三个数据。当前节点处理完之后要往哪里递归答案是左子树和右子树。但先去哪、后去哪以及访问根夹在中间哪个位置就是三种遍历的区别所在。整体的一个模板还是这样的functraverse(node*TreeNode){ifnodenil{return}// 位置 A前序在这里访问根traverse(node.Left)// 递归左子树// 位置 B中序在这里访问根traverse(node.Right)// 递归右子树// 位置 C后序在这里访问根}记住这三个位置 A、B、C——它们就是前序、中序、后序的全部秘密。下面我们来看一道题目深入理解一下给定一个二叉树的根节点root返回它的中序遍历。示例 1输入root [1,null,2,3] 输出[1,3,2]示例 2输入root [] 输出[]示例 3输入root [1] 输出[1]提示树中节点数目在范围[0, 100]内-100 Node.val 100遍历的规则在动手写代码之前先想清楚一次合法的遍历到底要满足什么。二叉树的递归定义决定了三件事空节点是递归的终点遇到nil直接返回这是所有遍历方式共用的终止条件访问顺序决定遍历名称根在左之前叫前序根在左右之间叫中序根在右之后叫后序左右子树的递归结构相同每个节点都把自己当成一棵新的子树来处理第 2 条尤其关键。比如示例 1 的[1,null,2,3]中序遍历之所以输出[1,3,2]是因为先递归到1的左子树空然后访问1再递归到1的右子树节点2在2这里先递归到它的左子树节点3访问3再访问2最后递归2的右子树空。整个顺序就是左 → 根 → 右。首先我们不需要排序也不需要一维数组。这道题的输入是一棵二叉树没有候选数组所以组合总和里的sort.Ints和startIndex在这里都用不上。这正说明二叉树题有自己的一套骨架不必硬套回溯。其次我们要定义几个数据一个是结果集用来收集遍历到的节点值res:[]int{}注意这里不像单词搜索那样传一个index因为树的结构本身就在递归栈里我们只需要一个地方把节点值存下来即可。一个是递归函数本身它的参数是当前节点functraverse(node*TreeNode)注意这里不像括号生成那样传open, close两个计数器因为树题要处理的是节点而不是数量。上面两个是确定的最后一个看题目不同来自己确定。这道题我们需要一个闭包来捕获结果集vartraversefunc(node*TreeNode)traversefunc(node*TreeNode){ifnodenil{return}// 中序左 → 根 → 右traverse(node.Left)resappend(res,node.Val)traverse(node.Right)}有了上面的数据我们现在来套用模板来写这道题目functraverse(node*TreeNode){首先是这个大框架终止条件肯定要判 nodenil这道题的当前节点要做什么不是数组而是由 node 动态决定的-访问根节点的值插入到左子树和右子树之间}if满足终止条件{这里的终止条件就是 nodenil直接返回不做任何访问}//这里我们要想如果还能往下走怎么办呢显然继续下面的递归即可//那如果左右子树都为空呢那就自然返回让上一层去处理// 遍历三个动作顺序由遍历方式决定traverse(node.Left)// 动作一递归左子树resappend(res,node.Val)// 动作二访问根中序traverse(node.Right)// 动作三递归右子树注意一个细节这道题和回溯题不一样——递归的入口不是某个固定起点而是整棵树的根节点。所以最外层只需要调用一次varres[]inttraverse(root)returnres只要中途node变成了nil就直接返回不用继续往下走了——这就是遍历整棵树和搜索特定路径的区别。因此我们最后改造的函数就是/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */funcinorderTraversal(root*TreeNode)[]int{res:[]int{}vartraversefunc(node*TreeNode)traversefunc(node*TreeNode){// 终止条件当前节点为空ifnodenil{return}// 中序左 → 根 → 右traverse(node.Left)// 递归左子树resappend(res,node.Val)// 访问根节点traverse(node.Right)// 递归右子树}traverse(root)returnres}进阶能不能不用闭包用显式的栈可以。递归的本质是编译器帮我们维护了一个调用栈我们完全可以自己用stack来模拟这个过程。中序遍历的迭代版本稍微有点绕因为访问根这个动作要延迟到左子树处理完之后。funcinorderTraversal(root*TreeNode)[]int{res:[]int{}stack:[]*TreeNode{}cur:rootforcur!nil||len(stack)0{// 一路向左把沿途节点压栈forcur!nil{stackappend(stack,cur)curcur.Left}// 弹出栈顶访问它curstack[len(stack)-1]stackstack[:len(stack)-1]resappend(res,cur.Val)// 转向右子树curcur.Right}returnres}这份代码比递归版本更长但把递归栈这个隐式结构显式化了——这就是递归和迭代的对应关系。尤其注意stack append(stack, cur)和stack stack[:len(stack)-1]这一对操作它们分别对应递归里的进入子树和从子树返回。拓展前序、中序、后序遍历的逻辑三种遍历共用同一套递归骨架唯一的区别是**访问根节点这一动作放在位置 A、B 还是 C**。用一张表就能看清遍历方式访问顺序访问根的位置示例 1 输出前序根 → 左 → 右位置 A递归左子树之前[1,2,3]中序左 → 根 → 右位置 B左右子树之间[1,3,2]后序左 → 右 → 根位置 C递归右子树之后[3,2,1]对应的代码只需要调换三行// 前序根 → 左 → 右funcpreorderTraversal(root*TreeNode)[]int{res:[]int{}vartraversefunc(node*TreeNode)traversefunc(node*TreeNode){ifnodenil{return}resappend(res,node.Val)// 位置 A访问根traverse(node.Left)// 递归左子树traverse(node.Right)// 递归右子树}traverse(root)returnres}// 中序左 → 根 → 右funcinorderTraversal(root*TreeNode)[]int{res:[]int{}vartraversefunc(node*TreeNode)traversefunc(node*TreeNode){ifnodenil{return}traverse(node.Left)// 递归左子树resappend(res,node.Val)// 位置 B访问根traverse(node.Right)// 递归右子树}traverse(root)returnres}// 后序左 → 右 → 根funcpostorderTraversal(root*TreeNode)[]int{res:[]int{}vartraversefunc(node*TreeNode)traversefunc(node*TreeNode){ifnodenil{return}traverse(node.Left)// 递归左子树traverse(node.Right)// 递归右子树resappend(res,node.Val)// 位置 C访问根}traverse(root)returnres}为什么顺序一换结果就完全不同因为二叉树的递归定义是根 左子树 右子树而遍历的本质就是决定在递归的哪一步处理根。前序在进入子树之前处理根中序在左子树回来之后处理根后序在右子树回来之后处理根。迭代版本的区别同样体现在访问根的时机上前序迭代用栈先压右再压左弹出即访问中序迭代用栈一路向左压栈弹出时访问再转向右后序迭代用栈按根 → 右 → 左压栈最后反转结果复杂度分析时间复杂度O(n)其中n是节点数。每个节点恰好被访问一次。空间复杂度O(h)其中h是树的高度。递归栈深度等于树高最坏情况下链式树为 O(n)平均情况下平衡树为 O(log n)。总结回过头看这道题的核心就是三个位置 A、B、C遍历方式访问根的位置代码体现前序位置 Ares append(res, node.Val)放在两次traverse之前中序位置 Bres append(res, node.Val)放在两次traverse之间后序位置 Cres append(res, node.Val)放在两次traverse之后和前面的回溯题对比一下区别一目了然组合总和括号生成单词搜索二叉树遍历核心结构一维数组两个计数器二维网格递归树终止条件curSum targetlen(path) 2*nindex len(word)node nil访问时机收集所有解收集所有解找到一条就收手位置 A/B/C 决定遍历方式关键操作做选择 / 撤销选择做选择 / 撤销选择做选择 / 撤销选择递归左 / 访问根 / 递归右遍历是二叉树的地基。把中序的递归逻辑想清楚前序和后序只是把append换一行的事把递归栈和显式栈的对应关系想清楚迭代版本也就不再神秘。后面的层序遍历、路径求和、最近公共祖先都会在这个骨架上继续长。本文是 《算法题目解析系列》 的第 [39] 篇本系列将持续更新每篇都提供清晰的思路与编程语言实现。欢迎关注第一时间获取更新。如果你有想看的题目也可以在评论区留言告诉我。
企业数字化 ERP 产品动态
相关推荐
爆款旅游攻略封面照着做!国庆节旅游攻略封面AI生图工具推荐 国庆假期还没开始,第一批旅游攻略已经在朋友圈、小红书和群聊里卷起来了。但真到自己做攻略封面,往往又是另一回事。照片拍了几十张,挑不出一张能当封面的;景点、路线、日期、城市名全想放上去,最后排成了“旅行社宣传… · 2026/9/24 3:08:36
《二本毕业生》第13章<被裁了> 第一天来上班,我早早就来到了公司,后来才知道公司是八点半上班,我七点四十五就到了,因为公司刚好离我住的地方不远,走路也就二十分钟左右。到了公司,已经有一个人在公司工位上了,我一进门&#… · 2026/9/24 3:08:29
固态激光雷达测距测绘原理与工程落地指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 3:08:29
Linux 常用开发工具:linux-command 私有化部署 引言
背景:开发运维需要大量常用命令和工具,频繁切换在线工具不便核心价值:一站式 Linux 命令查询平台,支持私有化部署适用场景:内部知识库、开发团队工具集、运维文档中心
前置条件
系统要求 Docker 引擎 19.03网络… · 2026/9/24 5:50:02
参数化设计平台技术拆解:从零件级模板库到 BOM 自动生成的完整链路 一、背景:非标设计的数据问题本质
非标装备制造的设计流程有个鲜明特点:约 80% 的结构是重复的,但每个订单都被当成新项目从头走一遍。
由此带来的典型工程问题:现象数据层面的根因设计复用率低、重复建模结构知识没有可复用载体通… · 2026/9/24 5:49:56
MSVCR100.dll丢失?VC++运行库缺失原因与修复方法详解 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 5:49:50
Qt手写轻量级HTTP服务器:从TCP到RESTful API实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 5:49:50
Ubuntu 20.04 sources.list 修复与软件源配置完全指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 5:49:19
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44