二叉树通关指南:用递归五部曲拆解七大类型
哈喽,大家好呀,我是蔚蓝,今天我们来聊聊二叉树。
你有没有过这种体验?打开 LeetCode,信心满满地点开一道二叉树的题,结果盯着屏幕半天,递归函数写了删、删了写,脑子里一团浆糊——这个返回值到底该是 int 还是 TreeNode?终止条件写 null 够不够?为什么别人的三行代码就能跑通,我写三十行还报空指针?
这不是你的智力问题,是你缺一个统一的思考框架。
我刷完二叉树 35 道经典题之后,把所有题目复盘了一遍,发现一个很有意思的事情:这些题看似五花八门,其实全在同一个套路里打转。而我将这个套路归纳为:递归五部曲。今天我把这五步拆开揉碎,配上七大题型,帮你掰开了,揉碎了讲清楚。
递归五部曲:别再凭感觉写代码了
很多人写递归就像蒙着眼睛炒菜,盐放多少全靠手感。递归五部曲就是给你一副精确的量杯:
第一步:确定递归函数的含义
这是最容易被跳过的一步。你拿到题目,别急着敲代码,先问自己一句话:「这个函数到底在算什么?」
比如求最大深度,maxDepth(root) 的含义是「以 root 为根的树的最大深度」。听起来像废话?但你往后看就知道,很多题卡住不是因为逻辑难,而是含义没定清楚。
第二步:确定入参和返回结构
入参决定了你递归过程中能访问到什么信息,返回值决定了你能把什么结果向上汇报。有些题需要返回一个数字,有些题需要返回一个节点,还有些题(比如打家劫舍 III)需要返回一个数组,把「偷」和「不偷」两种状态都带上去。
第三步:信任递归
这一步是心理建设,也是最重要的一步。
什么意思?假设你在求树的最大深度,你已经知道 maxDepth(root.left) 能正确返回左子树的深度,你也知道 maxDepth(root.right) 能正确返回右子树的深度。别去想它内部是怎么做到的,你只需要信任它,然后专注于当前节点该干什么。
这就像你在餐厅后厨,你是主厨,你让帮厨去切配菜。你不需要盯着他切的每一刀,你只需要相信切好的菜会出现在你面前,然后你来做你的那道工序。
第四步:确定递归的顺序
二叉树有三种经典遍历顺序,选哪一种取决于你的需求:
- 前序(中左右):适合「从上往下」传递信息的场景,比如求深度
- 中序(左中右):BST 的专属顺序,天然有序
- 后序(左右中):适合「从下往上」汇总信息的场景,比如求高度
记住一个口诀:深度用前序,高度用后序,BST 搜中序。
第五步:确定单层递归的逻辑
走到这一步就简单了。你已经知道函数含义、入参返回值、遍历顺序,也信任了子问题的结果。现在只需要写当前节点这「一层」该做的事情——处理自己,然后交给下一层。

七大题型实战拆解
光说不练假把式。接下来我按照七大题型,逐个用五部曲拆给你看。每道题我只挑最核心的点讲,代码里该有的注释一个不少。
类型一:递归遍历——认识你的树
遍历是二叉树的地基。前序(中左右)、中序(左中右)、后序(左右中),三种顺序,一个模子。
拿前序遍历举例,五部曲走一遍:
- 函数含义:遍历以 node 为根的子树,把节点值按前序顺序收集到 res 里
- 入参和返回:入参是当前节点 + 结果列表,无返回值(直接往列表里塞)
- 信任递归:
preorder(node.left)会正确处理左子树,我不关心细节 - 遍历顺序:前序,所以先处理自己,再递归左右
- 单层逻辑:把自己加入结果,然后左、右
▼java复制代码// 前序遍历 void preorder(TreeNode node, List<Integer> res) { if (node == null) return; // 终止条件:空节点不处理 res.add(node.val); // 中 —— 先把自己搞定 preorder(node.left, res); // 左 —— 信任递归,交给左子树 preorder(node.right, res); // 右 —— 信任递归,交给右子树 }
你会发现,中序和后序的代码结构一模一样,唯一的区别就是 res.add(node.val) 放在哪个位置。遍历的三种顺序,本质就是「什么时候处理自己」的选择题。

类型二:属性求值——深度、高度和对称性
这类题是递归五部曲的最佳练手场,因为它们的含义定义特别清晰。
最大深度(LeetCode 104)——五部曲拆解:
- 函数含义:
maxDepth(root)返回以 root 为根的树的最大深度 - 入参和返回:入参一个节点,返回 int
- 信任递归:
maxDepth(root.left)就是左子树的最大深度,maxDepth(root.right)就是右子树的最大深度 - 遍历顺序:后序!因为我需要先知道左右子树的深度,才能算当前节点的深度
- 单层逻辑:取左右深度的较大值,加 1(算上自己这一层)
▼java复制代码public int maxDepth(TreeNode root) { if (root == null) return 0; // 空树深度为 0 int left = maxDepth(root.left); // 信任左子树的结果 int right = maxDepth(root.right); // 信任右子树的结果 return Math.max(left, right) + 1; // 当前层 = max(左, 右) + 1 }
三行核心代码,清清爽爽。
但你注意,最小深度(LeetCode 111)有个大坑。很多人直接把 max 改成 min 就交了,结果挂在 [1, 2] 这种测试用例上——左子树深度 0,右子树深度 1,min(0, 1) + 1 = 1,但正确答案是 2,因为根节点的左孩子不是叶子节点,你根本不能走那条路。
避坑指南:单侧子树为空时,只能走另一侧。这不是取 min 的问题,是你还没搞清楚「叶子节点」的定义——左右孩子都为空才叫叶子。
▼java复制代码public int minDepth(TreeNode root) { if (root == null) return 0; int left = minDepth(root.left); int right = minDepth(root.right); // 坑就在这里:单侧为空,只能走另一侧 if (root.left == null && root.right != null) return right + 1; if (root.left != null && root.right == null) return left + 1; return Math.min(left, right) + 1; }

类型三:翻转与对称——动手改结构
翻转二叉树(LeetCode 226) 就是那道著名的「Homebrew 作者白板写不出来被拒」的题。说白了就是交换每个节点的左右孩子。
用五部曲思考:
- 函数含义:翻转以 root 为根的二叉树
- 入参和返回:一个节点,返回翻转后的根节点
- 信任递归:
invertTree(root.left)已经把左子树翻好了,invertTree(root.right)也一样 - 遍历顺序:前序(先交换当前节点的左右,再递归处理子树)
- 单层逻辑:交换左右指针
▼java复制代码public TreeNode invertTree(TreeNode root) { if (root == null) return null; // 先交换当前节点的左右孩子 TreeNode tmp = root.left; root.left = root.right; root.right = tmp; // 再递归处理子树 invertTree(root.left); invertTree(root.right); return root; }
对称二叉树(LeetCode 101) 的思路稍有不同——你需要同时比较两棵子树。函数含义变成「判断两棵子树是否镜像对称」,入参是两个节点。
▼java复制代码boolean compare(TreeNode left, TreeNode right) { if (left == null && right == null) return true; // 都空,对称 if (left == null || right == null || left.val != right.val) return false; // 只有一个空或值不等 // 外侧比外侧,内侧比内侧 return compare(left.left, right.right) && compare(left.right, right.left); }
注意这里的比较逻辑:左的左 和 右的右 比(外侧),左的右 和 右的左 比(内侧)。这就是「镜像」的含义——从外到内,完全翻转着比。
类型四:构造二叉树——从碎片到完整
这类题的经典模式是:给你两串遍历结果,让你还原出原来的树。
从中序与后序构造二叉树(LeetCode 106),五部曲:
- 函数含义:根据中序和后序数组的一个区间,构造出一棵子树
- 入参和返回:两个数组 + 各自的左右边界,返回构造出的根节点
- 信任递归:左半区间能正确构造出左子树,右半区间能正确构造出右子树
- 遍历顺序:后序的最后一个元素永远是根节点,用它在中序中切分左右
- 单层逻辑:找到根节点,在中序中定位,切分区间,递归构造
▼java复制代码TreeNode build(int[] post, int postL, int postR, int inL, int inR) { if (postL > postR || inL > inR) return null; TreeNode root = new TreeNode(post[postR]); // 后序最后一个是根 int idx = map.get(post[postR]); // 在中序中找到根的位置 int rightLen = inR - idx; root.left = build(post, postL, postR - rightLen - 1, inL, idx - 1); root.right = build(post, postR - rightLen, postR - 1, idx + 1, inR); return root; }
这道题的精髓在于区间的切割。后序数组里,左子树的区间怎么切、右子树的区间怎么切,全靠中序数组中根节点的位置来算。算错了就全盘崩溃。
一眼识别:遇到「给两种遍历序列构造二叉树」的题,套路都是——找根、切分、递归。前序+中序是第一个元素当根,后序+中序是最后一个元素当根,别的完全一样。

类型五:路径问题——从根走到叶
二叉树的所有路径(LeetCode 257),这道题的坑在于回溯。
五部曲分析:
- 函数含义:从当前节点出发,收集所有到叶子节点的路径
- 入参和返回:当前节点 + 当前路径字符串 + 结果列表
- 信任递归:递归到左右子树时,它们会正确地把路径补全
- 遍历顺序:前序,因为你要「从根往下走」
- 单层逻辑:把当前节点加进路径,到叶子就存结果,否则继续递归
▼java复制代码void traversal(TreeNode node, String path, List<String> res) { path += node.val; if (node.left == null && node.right == null) { res.add(path); // 到叶子了,保存路径 return; } if (node.left != null) traversal(node.left, path + "->", res); if (node.right != null) traversal(node.right, path + "->", res); }
这道题的巧妙之处在于,因为 Java 字符串是不可变的,每次 path + "->" 都产生了新对象,所以天然不需要手动回溯。但如果你换成 StringBuilder,那就必须手动删掉追加的部分——这就是回溯的经典操作。
类型六:二叉搜索树(BST)——自带排序的树
BST 的性质一句话概括:左小右大,中序有序。这个性质让你在很多题上可以简化判断。
验证 BST(LeetCode 98)——中序遍历判断是否严格递增:
▼java复制代码TreeNode pre = null; public boolean isValidBST(TreeNode root) { if (root == null) return true; if (!isValidBST(root.left)) return false; if (pre != null && pre.val >= root.val) return false; // 不是严格递增 pre = root; // 更新前驱 return isValidBST(root.right); }
这道题用了一个 pre 指针记录中序遍历的前驱节点。BST 的中序遍历一定是严格递增的,只要你发现当前节点不比前驱大,直接返回 false。
BST 的最近公共祖先(LeetCode 235) 更是优雅:
▼java复制代码public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { // p 和 q 都比 root 小,答案在左子树 if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q); // p 和 q 都比 root 大,答案在右子树 if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q); // 一个大一个小(或等于),说明分叉了,root 就是公共祖先 return root; }
BST 的 LCA 比「普通二叉树的 LCA(LeetCode 236)」简单很多,因为你不需要遍历完整棵树,直接根据大小关系砍掉一半的搜索空间。这就是 BST 自带「导航」的威力。
类型七:公共祖先——自底向上的汇报
普通二叉树的最近公共祖先(LeetCode 236) 是二叉树里思维难度最高的一道题之一。
五部曲走一遍:
- 函数含义:在以 root 为根的树中,找 p 和 q 的最近公共祖先
- 入参和返回:root、p、q,返回找到的祖先节点(或者 p/q 本身)
- 信任递归:
lowestCommonAncestor(root.left, p, q)能在左子树找到目标,右子树同理 - 遍历顺序:后序!必须先知道左右子树的搜索结果,才能判断当前节点是不是祖先
- 单层逻辑:左右都找到了 → 当前就是 LCA;只找到一边 → 返回找到的那一边
▼java复制代码public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) return root; // 找到了或到底了 TreeNode left = lowestCommonAncestor(root.left, p, q); // 左子树搜搜看 TreeNode right = lowestCommonAncestor(root.right, p, q); // 右子树搜搜看 if (left != null && right != null) return root; // 两边都找到了,我就是祖先 return left != null ? left : right; // 只有一边有结果,往上传递 }
这道题的精髓在第 5 行:if (left != null && right != null) return root。这意味着 p 和 q 分别在当前节点的两棵子树中,当前节点就是它们的最近公共祖先。
这也是「后序遍历」最典型的应用——你需要「先收集子树的信息,再做判断」,无法提前返回。
一张表总结
| 题型 | 遍历顺序 | 五部曲关键点 |
|---|---|---|
| 递归遍历 | 前/中/后序 | 含义简单:收集节点值。区别仅在处理顺序 |
| 深度与高度 | 前序求深度,后序求高度 | 含义定义是关键,最小深度注意单侧为空的坑 |
| 翻转与对称 | 前序翻转,后序比较 | 翻转即交换,对称即镜像比较 |
| 构造二叉树 | 前序(先建根再建子树) | 区间切割是难点,用 HashMap 加速查找 |
| 路径问题 | 前序 + 回溯 | 从根往下走,到叶子记录路径 |
| BST 操作 | 中序为主 | 利用「左小右大」性质,天然有序 |
| 公共祖先 | 后序 | 自底向上汇报,左右都有结果时当前节点就是答案 |
题目速查表
下面是文章中涉及的所有 LeetCode 题目,按七大题型分类,方便你按需跳转练习:
递归遍历
| # | 题目 | 难度 | 链接 |
|---|---|---|---|
| 144 | 二叉树的前序遍历 | 简单 | LeetCode |
| 94 | 二叉树的中序遍历 | 简单 | LeetCode |
| 145 | 二叉树的后序遍历 | 简单 | LeetCode |
层序遍历
| # | 题目 | 难度 | 链接 |
|---|---|---|---|
| 102 | 二叉树的层序遍历 | 中等 | LeetCode |
| 107 | 二叉树的层序遍历 II | 中等 | LeetCode |
| 199 | 二叉树的右视图 | 中等 | LeetCode |
| 637 | 二叉树的层平均值 | 简单 | LeetCode |
| 429 | N 叉树的层序遍历 | 中等 | LeetCode |
| 515 | 在每个树行中找最大值 | 中等 | LeetCode |
| 116 | 填充每个节点的下一个右侧节点指针 | 中等 | LeetCode |
属性求值
| # | 题目 | 难度 | 链接 |
|---|---|---|---|
| 104 | 二叉树的最大深度 | 简单 | LeetCode |
| 111 | 二叉树的最小深度 | 简单 | LeetCode |
| 222 | 完全二叉树的节点个数 | 中等 | LeetCode |
| 110 | 平衡二叉树 | 简单 | LeetCode |
| 257 | 二叉树的所有路径 | 简单 | LeetCode |
翻转与对称
| # | 题目 | 难度 | 链接 |
|---|---|---|---|
| 226 | 翻转二叉树 | 简单 | LeetCode |
| 101 | 对称二叉树 | 简单 | LeetCode |
构造二叉树
| # | 题目 | 难度 | 链接 |
|---|---|---|---|
| 106 | 从中序与后序遍历序列构造二叉树 | 中等 | LeetCode |
| 105 | 从前序与中序遍历序列构造二叉树 | 中等 | LeetCode |
| 654 | 最大二叉树 | 中等 | LeetCode |
| 617 | 合并二叉树 | 简单 | LeetCode |
二叉搜索树(BST)
| # | 题目 | 难度 | 链接 |
|---|---|---|---|
| 700 | 二叉搜索树中的搜索 | 简单 | LeetCode |
| 98 | 验证二叉搜索树 | 中等 | LeetCode |
| 530 | 二叉搜索树的最小绝对差 | 简单 | LeetCode |
| 501 | 二叉搜索树中的众数 | 简单 | LeetCode |
| 235 | 二叉搜索树的最近公共祖先 | 中等 | LeetCode |
| 701 | 二叉搜索树中的插入操作 | 中等 | LeetCode |
| 450 | 删除二叉搜索树中的节点 | 中等 | LeetCode |
| 669 | 修剪二叉搜索树 | 中等 | LeetCode |
| 108 | 将有序数组转换为二叉搜索树 | 简单 | LeetCode |
| 538 | 把二叉搜索树转换为累加树 | 中等 | LeetCode |
公共祖先
| # | 题目 | 难度 | 链接 |
|---|---|---|---|
| 236 | 二叉树的最近公共祖先 | 中等 | LeetCode |
碎碎念
刷二叉树,最怕的不是题目难,而是你没有统一的思考框架。每道题都从头开始「感觉应该这样写」,写了删、删了写,效率低到令人崩溃。
递归五部曲就是你的「算法量杯」。拿到任何一道二叉树的题,别急着写代码,先按五步走一遍:函数含义是什么、入参返回值怎么定、我能不能信任子问题的结果、该用前序还是后序、当前层要做什么。五步走完,代码基本就出来了。
你回头看看上面每一道题,是不是都是这个套路?35 道题,一个模子。
递归从来不是玄学,它只是一种 「定义清楚、信任子问题、只管当前层」 的思维方式。一旦你接受了这种思维,二叉树的题目就不是在考你算法能力了,而是在考你够不够冷静。
以上,希望能帮你少走点弯路。刷题路上,一起加油 💪

