二叉树 dfs 题型总结

目录

[引入:dfs 中最先要判断的信息方向](#引入:dfs 中最先要判断的信息方向)

一、计算布尔二叉树的值:先算孩子,再处理运算节点

二、求根节点到叶节点数字之和:把累计值向下传递

三、二叉树剪枝:先处理子树,再决定当前节点是否删除

四、验证二叉搜索树:利用中序遍历的递增性

[五、二叉搜索树中第 K 小的元素:中序遍历加计数](#五、二叉搜索树中第 K 小的元素:中序遍历加计数)

[六、从多道二叉树题中归纳 DFS 规律](#六、从多道二叉树题中归纳 DFS 规律)

七、易错点

八、本篇总结


引入:dfs 中最先要判断的信息方向

二叉树中的 dfs 是 Depth First Search 的缩写,中文是深度优先搜索。它表示先沿着一条树枝向下处理,走到不能继续的位置后,再返回处理另一条树枝。

二叉树递归题经常有两种信息流向:一种是父节点已经得到的信息向下传给孩子,例如根到当前节点已经组成的数字;另一种是左右子树先分别计算结果,再由当前节点汇总,例如布尔运算和剪枝。

下面代码常出现的变量:

代码中常见的 root 表示当前递归处理的节点,preSum 表示从根节点到父节点已经累计出的数字,ret 表示当前方法准备返回的结果,leftright 有时表示左右子树计算出的结果。

选择前序、中序还是后序,关键取决于当前节点需要的信息来自哪里。

一、计算布尔二叉树的值:先算孩子,再处理运算节点

题目描述

题目:计算布尔二叉树的值。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

非叶子节点先递归得到 leftright 两个布尔结果。root.val == 2 时使用或运算 |,否则使用与运算 &。这里的 leftright 不是左右节点本身,而是左右子树已经计算出的真假结果。

二、求根节点到叶节点数字之和:把累计值向下传递

题目描述

题目:求根节点到叶节点数字之和。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.leftroot.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.valprev 比较;只有当前值更大,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 保存答案。写代码前先说清楚返回值代表什么,递归结构通常就会清楚很多。

七、易错点

  1. 访问 root.leftroot.right 前忘记处理 root == null
  2. 需要子树结果的题过早处理当前节点。
  3. 剪枝后忘记把返回的新子树接回父节点。
  4. 验证 BST 时只比较直接孩子,没有考虑祖先对整棵子树的约束。
  5. prevcount 这类成员变量在多次调用之间没有重新初始化。
  6. 路径数字传递给孩子前忘记执行 preSum * 10 + root.val
  7. 第 K 小元素的计数位置写错,导致少数一位或多算一位。

八、本篇总结

二叉树 DFS 题可以先从信息方向判断遍历顺序:

  • 父节点信息向孩子传,通常是前序式;
  • 需要左右子树结果后再处理当前节点,通常是后序式;
  • 利用 BST 从小到大的性质,就优先想到中序遍历。

遍历顺序不是孤立的口诀,而是当前节点所需信息的直接结果。

相关推荐
我变成萤火虫6 小时前
河南萌新联赛2026第(四)场:南阳理工学院
数据结构·c++·算法·贪心算法·stl·动态规划
To_OC8 小时前
LC 239 滑动窗口最大值:从暴力超时到单调队列一遍过
javascript·算法·leetcode
.道阻且长.9 小时前
9.LeetCode算法习题讲解--滑动窗口--无重复字符的最长字串
算法·leetcode·职场和发展·哈希算法
ltl11 小时前
HNSW:图索引如何击败树索引
算法
曹牧12 小时前
C#:数字的定义和表示方式
算法·c#
Nil20813 小时前
leetcode 138随机链表的复制
算法·leetcode·链表
疯狂打码的少年14 小时前
【数据结构】图的遍历:深度优先搜索(DFS)
数据结构·笔记·算法·深度优先
-凌凌漆-15 小时前
【freertos】Task创建(v2)
java·开发语言·算法