二叉树递归四题:翻转、对称、最大最小深度,面试高频考点 刷算法题这些年到了二叉树这一块最大的感触是题目的马甲换来换去核心其实就那几板斧——你懂不懂递归、懂不懂怎么把整棵树的问题拆成当前节点 左右子树的局部问题。今天集中把四道题过一遍226.翻转二叉树、101.对称二叉树、104.二叉树的最大深度、111.二叉树的最小深度。它们正好构成代码随想录训练营第12期的训练内容代码量不大但把二叉树递归题里最典型的思路和陷阱几乎全踩了一遍。如果你正在备战算法面试或者刚学完二叉树遍历、想找点题练手这四道题值得花一整晚彻底吃透。它们表面上彼此独立实际上共享同一套递归套路一个练交换操作一个练结构比较一个练信息的自底向上汇总最后一个练边界条件的处理。把这四个模型装进脑子里后面遇到二叉树相关的hard题你会发现底层逻辑很多都是这四道题的变体。1. 整体设计拆解四个二叉树题目为何要组队出现1.1 题目之间的递进关系这四道题不是随手拼在一起的它们的难度和思维模式有明显递进。226.翻转二叉树是最基础的全局操作题。你拿到一棵树要做的只是遍历每一个节点把它的左右孩子交换一下。它不要求你比较子树不要求返回值甚至没有特殊的边界陷阱。它唯一的作用是帮你建立遍历框架可以套用到任何操作上的认知。很多人在这一题会第一次意识到原来前序遍历的模板里把打印节点换成交换孩子就是一道新题。101.对称二叉树开始进入结构比较的领域。它不再要求你改变树而是要你判断两棵子树是否呈现镜像关系。这里第一次出现双指针同步递归的概念——同时走两棵树维护left和right两个指针比较它们的情况。这种成对节点的比较方式是后续很多二叉树题比如合并二叉树、判断相同的树的基础。104.二叉树的最大深度的思维模型又不一样。它要求子节点先把自己的深度算出来父节点再从中取最大值加一。这是一种典型的自底向上的折叠汇总跟后序遍历的执行顺序天然吻合。它也是递归三要素中返回值设计的最佳练习题。111.二叉树的最小深度看似只把max换成min实际上暗藏了一个非常容易翻车的边界条件如果左子树为空不能直接用min(0, rightDepth) 1因为0对应的是空子树而不是叶子。这道题逼着你去认真思考终止条件里的null代表什么。所以这一组题的正确定位是前两题练递归要做什么后两题练递归要返回什么。就这么四个字的变化四道题全齐了。1.2 这套组合覆盖的算法能力点我整理了一下这四道题主要覆盖的考点方便对照自测题号核心考点常用解法主要思维模型226前序/后序递归、迭代遍历递归交换、栈模拟全局操作101双指针递归比较、层序比较递归、队列迭代结构比较104后序递归、层序遍历递归取max、队列计数自底向上汇总111边界条件处理、层序剪枝递归特判、队列找叶子边界与剪枝刷完这一组你应该能回答自己三个问题递归函数的参数和返回值怎么设计终止条件怎么写才是完备的单层递归逻辑里是先处理当前节点还是先递归子树这三个问题一旦明了二叉树大部分题目都能自己推出来。2. 前置概念把深度、高度和递归三要素一次性讲透2.1 深度与高度的区别LeetCode 104说的最大深度指的是根节点到最远叶子节点的路径上的节点个数。比如只有一个根节点深度就是1空树深度是0。但深度和高度这两个词在算法里经常混着用建议还是按教科书区分开深度是从根节点向下数高度是从叶子节点向上数。一棵树的高度等于根节点的高度同时也等于这棵树的最大深度。所以在104题里你既可以按求深度的思维做层序累加也可以按求高度的思维做后序递归最终答案一样。实操建议面试时尽量用后序递归去解释因为代码能直接对应左右子树先算完当前节点再汇总的过程。如果面试官追问层序再说层序同能求复杂度都是O(n)。2.2 递归三要素与后序遍历的天然优势很多新手写递归喜欢一个劲往里钻结果绕晕了。我建议所有递归都按这三步来确定递归函数的参数与返回值。确定终止条件。确定单层递归的逻辑。用后序遍历举例我要算一个树的深度递归函数参数就是当前节点返回值是以当前节点为根的子树深度。终止条件是节点为空返回0。单层逻辑是先拿左子树的深度再拿右子树的深度取较大值加1。这跟后序遍历左右根的执行顺序完全一致因为你只有拿到左右子树的结果才能算出当前节点的结果。三个要素的任何一个写错递归大概率会出问题。参数设计代表递归要处理谁返回值代表要给上层反馈什么终止条件代表递归什么时候停下来。224和226考的就是第三点104和111考的就是第二点。把这套分析框架练熟了比背十道题有用得多。3. 四道题逐一拆解与手写实现3.1 226.翻转二叉树全局交换思维这道题曾让某个知名程序员在推上吐槽说Homebrew的作者因为不会翻转二叉树而没拿到谷歌offer所以也别轻视它。考察点很单纯你要对每一个节点执行交换左右孩子这个动作然后继续处理它的孩子。递归版本我最推荐前序TreeNode* invertTree(TreeNode* root) { if (root nullptr) return nullptr; swap(root-left, root-right); invertTree(root-left); invertTree(root-right); return root; }这里把交换放在递归之前是典型的前序。先处理当前节点再递归到左右子树。你也可以把交换放到两个递归之后变成后序一样能通过。真正不建议用中序因为中序的顺序是左、中、右你先把左子树翻转完交换了当前节点的左右孩子再递归处理右子树时当前节点的右子树已经不是原来的右子树而是原来的左子树。结果就是原右子树根本没被翻转原左子树被颠来倒去处理了两次部分节点等于没翻。这个细节面试官偶尔会拿出来聊能讲清楚绝对是加分项。如果想练习迭代法可以用栈模拟前序TreeNode* invertTree(TreeNode* root) { stackTreeNode* st; if (root) st.push(root); while (!st.empty()) { TreeNode* node st.top(); st.pop(); swap(node-left, node-right); if (node-left) st.push(node-left); if (node-right) st.push(node-right); } return root; }复杂度是O(n)每个节点访问一次。递归栈空间是O(h)h是树高最坏情况退化成链表时是O(n)。这一题的价值不在难度而在于让你建立遍历模板改一行就是一道新题的直觉。3.2 101.对称二叉树内外侧比较思维对称二叉树的核心不是比较左孩子和右孩子是否相等而是比较两棵子树是否互为镜像。具体地你要同时比较左子树的左孩子 vs 右子树的右孩子外侧对外侧左子树的右孩子 vs 右子树的左孩子内侧对内侧我写了太多遍这道题发现用递归比较最稳思路也最清晰。递归函数接收两个指针bool compare(TreeNode* left, TreeNode* right) { if (left nullptr right nullptr) return true; else if (left nullptr || right nullptr) return false; else if (left-val ! right-val) return false; bool outSide compare(left-left, right-right); bool inSide compare(left-right, right-left); return outSide inSide; } bool isSymmetric(TreeNode* root) { if (root nullptr) return true; return compare(root-left, root-right); }注意终止条件的顺序先判断都为空再判断一空一不空最后判断值是否不等。这个顺序不能乱一旦反过来就会出现空指针访问。很多人写这道题报运行时错误基本都是因为先判断了值忘了此时left或right可能已经是nullptr。迭代法也不复杂但思维要转换用队列保存一对一对的节点每次出队两个比较再把它们的左右孩子按比较顺序分别入队。这个思路本质上是层序地把对称节点放到相邻位置。面试追问迭代写法时可以直接答出来能明显加分。3.3 104.二叉树的最大深度后序递归的标准模板最大深度的递归解法几乎是二叉树递归的hello worldint maxDepth(TreeNode* root) { if (root nullptr) return 0; int leftDepth maxDepth(root-left); int rightDepth maxDepth(root-right); return max(leftDepth, rightDepth) 1; }为什么用后序因为当前节点的深度必须由左右子树的深度推导出来。先从左边拿到深度再从右边拿到深度取大值加一。换个角度想这题也可以用层序遍历做一层一层往下数每遍历一层深度加一直到队列为空。int maxDepth(TreeNode* root) { if (root nullptr) return 0; queueTreeNode* q; q.push(root); int depth 0; while (!q.empty()) { int size q.size(); depth; for (int i 0; i size; i) { TreeNode* node q.front(); q.pop(); if (node-left) q.push(node-left); if (node-right) q.push(node-right); } } return depth; }层序写法对理解深度这个词很直观你确实在从上往下逐层数。但递归写法更符合面试官对二叉树基本功的期待。我的建议是两种都会写然后根据树的形态决定解释哪一个讲递归时强调后序遍历顺序讲迭代时强调队列的一层一层处理。复杂度都是O(n)时间上每个节点进出一次。3.4 111.二叉树的最小深度边界陷阱最集中的一题这题是四道题里最容易阴沟翻船的一道。先看一个看起来理所当然、实际上是错的解法int minDepth(TreeNode* root) { if (root nullptr) return 0; return min(minDepth(root-left), minDepth(root-right)) 1; }错在哪儿题目要求的是从根节点到最近叶子节点的最短路径上的节点数。叶子节点必须是没有孩子的节点而空指针不是叶子。如果一棵树只有左子树没有右子树那么右子树返回0min(左深度, 0)直接取到0结果会少算一条必须经过的路径。举个例子3 / 9这棵树的最小深度显然是2因为根节点到叶子9要经过两个节点。但错误解法计算min(深度9, 0)1 1答案错了。正确的递归解法需要在单层逻辑里对空子树做特殊处理int minDepth(TreeNode* root) { if (root nullptr) return 0; int leftDepth minDepth(root-left); int rightDepth minDepth(root-right); if (leftDepth 0) return rightDepth 1; if (rightDepth 0) return leftDepth 1; return min(leftDepth, rightDepth) 1; }这里的逻辑是如果某一侧子树为空就忽略它只走另一侧。两侧都为空也就是当前节点正好是叶子时综合算下来min(0, 0) 1 1没问题。层序解法反而更贴近最短路径的定义一层层往下走遇到的第一个叶子节点直接返回当前深度。这不用处理空子树的特判天然避开了那个大坑所以我推荐先写层序版本int minDepth(TreeNode* root) { if (root nullptr) return 0; queueTreeNode* q; q.push(root); int depth 0; while (!q.empty()) { int size q.size(); depth; for (int i 0; i size; i) { TreeNode* node q.front(); q.pop(); if (node-left nullptr node-right nullptr) { return depth; } if (node-left) q.push(node-left); if (node-right) q.push(node-right); } } return depth; }这里第一个叶子节点对应的就是最近的叶子所以直接返回深度。递归写法更考察边界思维迭代写法更直观两种都要掌握。4. 常见报错与排查技巧实录4.1 报错实录空指针、栈溢出、超时写二叉树程序时经常报警运行时错误我在训练营里见过最多的报错无非三类。第一类空指针访问。典型场景是递归函数里写if (root-val x)但压根没先判断 root 是否为 nullptr。在对称二叉树里最容易犯因为函数要接收两个指针。排查方法每个递归函数先想清楚传入的参数有没有可能是null有的话必须先把空值情况处理完再去访问成员。第二类栈溢出。递归没有终止条件或终止条件写得不对。比如有人把终止条件写成if (root nullptr) return true;结果某个分支又返回了compare(...)导致递归无法收敛。排查方法画出递归树看每一条路径是不是都在向叶子节点逼近逼近的终点是不是已经显式处理。第三类超时/死循环。二叉树天然有向按理说不容易死循环但迭代法如果节点入栈/入队后没有正确弹出或者使用栈时没有标记访问状态就可能无限循环。也遇到过一种情况递归函数返回了错误层级的比较结果导致重复对同一批节点递归。排查方法把递归深度打印出来看是不是在重复访问同样的节点。4.2 报错实录返回值丢失与顺序写反第二类高频问题不是运行时报错而是答案错得莫名其妙。递归函数声明了int却漏掉某一分支的return这在C里是未定义行为返回的会是垃圾值。仔细检查每一个if分支确保每个分支都有明确的return。另一个常见错误是递归顺序写反。求最大深度时有人会先return maxDepth(root-left) 1;再去递归右子树等于直接把右子树丢掉了。记住后序递归要求左右子树的返回值先算好当前节点才能汇总。4.3 报错实录最小深度为什么不能直接取min再强调一遍这个误区因为我见过太多人栽在这里。最小深度定义的是到叶子节点的路径长度空节点不算叶子。所以递归里不能用空子树返回0参与min运算这种逻辑必须对某侧子树为空做单独处理。实战中我建议你可以用一组最小case来自测空树返回0只有根节点返回1根只有左孩子返回2根只有右孩子返回2完全二叉树则按正常min。这组用例全过基本不会错。输入形态最大深度最小深度[]00[1]11[1,2,null]22[1,null,2]22[1,2,3]22[1,2,3,4,null,null,5]32最后一行的最小深度是2因为第二层的节点3已经是叶子。4.4 调试递归的实战方法我在本地和LeetCode上调试二叉树递归时比较实用的套路是这三个。第一画递归树。把一棵小树画出来对每个节点标注递归进入时做什么返回什么值。尤其适合对称二叉树把外层内层的比较路径画一遍代码自然就理顺了。第二打印关键信息。在每个递归函数入口打印当前节点值在return前打印返回值。这样能直接看到递归的执行顺序和返回路径。面试时打印不现实但自己练习时非常高效。第三设计最小用例集。不要一上来就测复杂树先用「空树、单节点、只有左子树、只有右子树、标准三层树」这五类模板去测能在最小范围内暴露绝大多数边界错误。5. 从刷题到面试这些题还能怎么考5.1 迭代法变式要能随时切换面试官可能会在你写完递归后追问一句如果树特别深递归会不会爆栈写个迭代版本。所以这四道题最好都准备一版迭代方案。翻转二叉树的迭代就是前序遍历模板用栈模拟把处理节点动作改成swap。对称二叉树的迭代是队列里成对压入节点成对比较。最大深度的迭代是层序逐层累加。最小深度的迭代是层序遇到第一个叶子直接返回。这四个迭代版本代码都不长建议自己写完而不是直接抄因为面试时你很难凭空默写别人代码。写完迭代后对比递归你会对什么时候该用队列、什么时候该用栈有更清楚的认识。5.2 这几道题是很多高频题的地基226翻转二叉树对应剑指Offer里的镜像二叉树面试中经常换个包装出现。101对称二叉树的比较逻辑可以扩展到判断两棵树是否相同甚至判断一棵树是不是另一棵树的子树。104最大深度的后序模板是平衡二叉树判断的前置技能——你需要在后序遍历里同时返回深度和是否平衡。111最小深度的层序解法则是二叉树最浅叶子层数这类BFS问题的标准思路。所以刷完这四题我建议你做一次加法把递归三要素写在一张便签上然后把四道题的递归函数参数、返回值、终止条件列出来对比。你会发现它们高度相似差异只在单层逻辑。这个发现比刷十道新题都值钱。我自己的体会是这四道题刷完之后不要急着冲hard题先停下里把四段代码每天默写一遍。二叉树递归最关键的就是建立信任——当你调用minDepth(root-left)时你要相信它返回的就是左子树的最小深度不要钻进递归里一层层拆解。想清楚这一层二叉树这一大类题的思路都会顺很多。后面再去碰那些加了一堆限制条件的复杂题你会发现底子多半还是今天这两页纸上的东西。