目录
[引入:dfs 中最先要判断的信息方向](#引入:dfs 中最先要判断的信息方向)
[五、二叉搜索树中第 K 小的元素:中序遍历加计数](#五、二叉搜索树中第 K 小的元素:中序遍历加计数)
[六、从多道二叉树题中归纳 DFS 规律](#六、从多道二叉树题中归纳 DFS 规律)
引入:dfs 中最先要判断的信息方向
二叉树中的 dfs 是 Depth First Search 的缩写,中文是深度优先搜索。它表示先沿着一条树枝向下处理,走到不能继续的位置后,再返回处理另一条树枝。
二叉树递归题经常有两种信息流向:一种是父节点已经得到的信息向下传给孩子,例如根到当前节点已经组成的数字;另一种是左右子树先分别计算结果,再由当前节点汇总,例如布尔运算和剪枝。

下面代码常出现的变量:
代码中常见的 root 表示当前递归处理的节点,preSum 表示从根节点到父节点已经累计出的数字,ret 表示当前方法准备返回的结果,left 和 right 有时表示左右子树计算出的结果。
选择前序、中序还是后序,关键取决于当前节点需要的信息来自哪里。
一、计算布尔二叉树的值:先算孩子,再处理运算节点
题目描述
题目:计算布尔二叉树的值。LeetCode 2331。
一棵二叉树的叶子节点值为 0 或 1,分别表示假和真;非叶子节点值为 2 或 3,分别表示"或"运算和"与"运算。计算整棵树最终得到的布尔值。
题目链接:计算布尔二叉树的值
算法原理
叶子节点已经是最终的真假结果,可以直接返回。非叶子节点代表一个运算,必须先知道左子树和右子树的结果,才能进行"或"或"与"运算,所以这里采用先左、再右、最后当前节点的后序结构。
Java 代码
java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
class Solution {
public boolean evaluateTree(TreeNode root) {
if (root.left == null) {
return root.val == 0 ? false : true;
}
boolean left = evaluateTree(root.left);
boolean right = evaluateTree(root.right);
return root.val == 2 ? left | right : left & right;
}
}
代码说明
root 是当前节点。若 root.left == null,说明当前节点是叶子节点,值为 0 时返回 false,值为 1 时返回 true。
非叶子节点先递归得到 left 和 right 两个布尔结果。root.val == 2 时使用或运算 |,否则使用与运算 &。这里的 left 和 right 不是左右节点本身,而是左右子树已经计算出的真假结果。
二、求根节点到叶节点数字之和:把累计值向下传递
题目描述
题目:求根节点到叶节点数字之和。LeetCode 129。
二叉树每个节点保存一个数字。从根节点走到叶节点会组成一个数字,例如路径 1 -> 2 -> 3 组成 123。求所有根到叶路径组成的数字之和。
题目链接:求根节点到叶节点数字之和
算法原理
走到一个新节点时,之前已经组成的数字要向左移动一位,再把当前节点值加到个位上,因此新的数字是 preSum * 10 + root.val。
如果当前节点是叶子,当前数字已经完整,直接返回。否则把更新后的数字分别传给左子树和右子树,最后把两边得到的结果相加。这里的信息是从父节点向孩子传递的,属于前序式的状态传递。
Java 代码
java
class Solution {
public int sumNumbers(TreeNode root) {
return dfs(root, 0);
}
public int dfs(TreeNode root, int preSum) {
preSum = preSum * 10 + root.val;
if (root.left == null && root.right == null) {
return preSum;
}
int ret = 0;
if (root.left != null) {
ret += dfs(root.left, preSum);
}
if (root.right != null) {
ret += dfs(root.right, preSum);
}
return ret;
}
}
代码说明
dfs(root, preSum) 的含义是:从当前节点继续走到所有叶节点,把这些路径组成的数字之和返回。preSum 是进入当前节点前,父节点路径已经组成的数字。
当前节点加入后,preSum 更新为 preSum * 10 + root.val。到达叶子时返回当前数字;没有到达叶子时,ret 从 0 开始,分别累加左右子树的结果。使用参数传递累计值,不需要使用成员变量,也不会因为多条路径共用一个变量而互相影响。
三、二叉树剪枝:先处理子树,再决定当前节点是否删除
题目描述
题目:二叉树剪枝。LeetCode 814。
给定一棵只包含 0 和 1 的二叉树,删除所有不包含 1 的子树。返回剪枝后的根节点。
题目链接:二叉树剪枝
算法原理
当前节点是否应该删除,取决于它剪枝后的左右子树是否为空,以及当前节点本身是否为 0。因此必须先递归处理左右子树,再把处理后的结果重新赋值给 root.left 和 root.right。
如果左右子树都为空、当前节点值为 0,说明这棵子树完全没有 1,可以返回 null 删除它。否则保留当前节点并返回 root。
Java 代码
java
class Solution {
public TreeNode pruneTree(TreeNode root) {
if (root == null) return null;
root.left = pruneTree(root.left);
root.right = pruneTree(root.right);
if (root.left == null &&
root.right == null &&
root.val == 0) {
root = null;
}
return root;
}
}
代码说明
pruneTree 的返回值是"当前子树剪枝完成后的根节点"。父节点通过 root.left = pruneTree(root.left) 和 root.right = pruneTree(root.right) 接回处理后的左右子树。
这里不能只看当前节点原来的孩子,因为孩子本身可能还需要继续剪枝。只有左右递归都完成后,当前节点是否变成值为 0 的叶子才是确定的。
四、验证二叉搜索树:利用中序遍历的递增性
题目描述
题目:验证二叉搜索树。LeetCode 98。
判断一棵二叉树是否满足二叉搜索树规则:左子树所有节点值都小于当前节点,右子树所有节点值都大于当前节点,并且这个关系要对整棵子树成立。
题目链接:验证二叉搜索树
算法原理
二叉搜索树的中序遍历顺序是"左子树、当前节点、右子树"。如果整棵树合法,按照这个顺序访问到的节点值一定严格递增。
因此可以用 prev 保存中序遍历刚刚访问过的前一个节点值。当前节点值如果不大于 prev,就说明中序序列不是严格递增,树不合法。prev 使用 long,可以覆盖 int 节点值的完整范围。
Java 代码
java
class Solution {
long prev = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
boolean left = isValidBST(root.left);
if (left == false) return false;
boolean cur = false;
if (root.val > prev) cur = true;
if (cur == false) return false;
prev = root.val;
boolean right = isValidBST(root.right);
return left && cur && right;
}
}
代码说明
递归先检查左子树。左子树不合法时,直接返回 false,这就是剪枝。回到当前节点后,将 root.val 和 prev 比较;只有当前值更大,cur 才会为真。
检查通过后,把 prev 更新为当前节点值,再检查右子树。这里不能只比较当前节点和直接孩子,因为更深层节点仍然可能违反祖先节点的范围限制。
实际使用这个成员变量版本时,如果同一个 Solution 对象连续调用 isValidBST,需要在每次公共方法开始时把 prev 重置为 Long.MIN_VALUE。在 LeetCode 的单次调用环境中通常只调用一次,但理解这一点有助于避免共享状态残留。
五、二叉搜索树中第 K 小的元素:中序遍历加计数
题目描述
题目:二叉搜索树中第 K 小的元素。LeetCode 230。
给定一棵二叉搜索树和整数 k,返回这棵树中按从小到大排列后的第 k 个节点值。
题目链接:二叉搜索树中第 K 小的元素
算法原理
因为 BST 的中序遍历结果从小到大排列,所以不需要把所有节点排序。每访问一个节点,就把 count 减 1;当 count 减到 0 时,当前节点就是第 K 小的元素。
答案找到后,后续子树已经没有继续访问的必要,可以直接返回。这是基于"答案已经确定"的剪枝。
Java 代码
java
class Solution {
int count;
int ret;
public int kthSmallest(TreeNode root, int k) {
count = k;
dfs(root);
return ret;
}
public void dfs(TreeNode root) {
if (root == null || count == 0) return;
dfs(root.left);
count--;
if (count == 0) {
ret = root.val;
}
dfs(root.right);
}
}
代码说明
count 表示还需要访问几个节点才能到达第 K 小,ret 保存找到的答案。dfs 先访问左子树,再处理当前节点,符合 BST 的中序顺序。
当 count == 0 时已经找到答案。虽然代码还会执行一次 dfs(root.right),但这个调用会因为 count == 0 立即返回。如果希望写得更严格,也可以在找到答案后直接 return。
六、从多道二叉树题中归纳 DFS 规律
这几道题的共同骨架都是从当前 root 出发递归处理子树,但当前节点的处理时机不同。
前序式处理适合把父节点信息传给孩子。根节点到叶节点数字和中,当前节点先更新 preSum,再把它传给左右孩子。
中序式处理适合利用 BST 的有序性质。验证 BST 通过 prev 检查严格递增,第 K 小元素通过 count 按从小到大计数。
后序式处理适合依赖子树结果。布尔树需要先取得左右真假值,剪枝需要先取得剪枝后的左右子树。
返回值也有不同含义:布尔树返回真假,路径和返回数字总和,剪枝返回处理后的子树根节点,BST 第 K 小通过 ret 保存答案。写代码前先说清楚返回值代表什么,递归结构通常就会清楚很多。
七、易错点
- 访问
root.left或root.right前忘记处理root == null。 - 需要子树结果的题过早处理当前节点。
- 剪枝后忘记把返回的新子树接回父节点。
- 验证 BST 时只比较直接孩子,没有考虑祖先对整棵子树的约束。
prev或count这类成员变量在多次调用之间没有重新初始化。- 路径数字传递给孩子前忘记执行
preSum * 10 + root.val。 - 第 K 小元素的计数位置写错,导致少数一位或多算一位。
八、本篇总结
二叉树 DFS 题可以先从信息方向判断遍历顺序:
- 父节点信息向孩子传,通常是前序式;
- 需要左右子树结果后再处理当前节点,通常是后序式;
- 利用 BST 从小到大的性质,就优先想到中序遍历。
遍历顺序不是孤立的口诀,而是当前节点所需信息的直接结果。