目录

题目描述

1325. 删除给定值的叶子节点

题意分析

给一棵二叉树和一个目标值 target,要求删掉所有值等于 target 的叶子节点。关键在于删除会传染:一个节点原本有孩子所以不是叶子,孩子被删光之后它自己就变成了叶子,如果它的值也等于 target,同样要被删掉,而且要一直这样连锁下去直到没有节点可删。

从这个描述能读出两个信号。一是判定条件依赖「删除之后的状态」而不是原始状态,所以任何自顶向下、只看一次的做法都会漏;二是删除操作会改变树的结构,需要有办法把「这棵子树没了」这个事实告诉父节点。

边界上最容易忽略的是根节点:如果整棵树最后只剩下根,而根的值恰好是 target,那么根也要被删掉,函数需要返回空树。所以返回类型必须是节点而不是 void

解法:后序遍历

核心思路

删除条件取决于孩子删除后的状态:孩子被删光后,父节点可能新变成值为 target 的叶子。因此要用后序遍历,先处理左右子树,再判断当前节点。

定义递归契约:removeLeafNodes(node, target) 返回“以 node 为根的子树完成所有连锁删除后的新根”;整棵子树被删空时返回 null。父节点把递归结果写回 leftright,拿到的就是两个孩子的最终状态。

此时若当前节点左右孩子都为空且值等于 target,返回 null;否则返回当前节点。一次回溯就能完成多层连锁删除,无需反复扫描整棵树。

正确性说明:对任意子树做归纳。空树显然处理正确;假设左右子树的递归结果都正确,那么写回后,当前节点的两个孩子正是删除完成后的结果。算法随后严格按题意删除“此刻值为 target 的叶子”,或保留当前节点,因此当前子树的返回结果也正确。由归纳可知根节点返回的就是最终答案。

解题步骤

  1. 遇到空节点,返回 null
  2. 递归处理左、右子树,并将返回的新根分别写回 root.leftroot.right
  3. 回溯到当前节点时,检查它是否已经成为叶子且 root.val == target;若是,返回 null 表示删除整棵当前子树。
  4. 否则返回 root,表示当前节点继续作为这棵子树的根。

连锁删除示例root = [1,2,null,2]target = 2。最底层的 2 先返回 null;上一层 2 写回孩子后也变成目标叶子,继续返回 null;根 1 的左指针最终被置空。每一层都只判断一次,但回溯顺序已经完成了重复删除。

代码实现

class Solution {
    public TreeNode removeLeafNodes(TreeNode root, int target) {
        if (root == null) {
            return null;
        }
        root.left = removeLeafNodes(root.left, target);
        root.right = removeLeafNodes(root.right, target);
        if (root.left == null && root.right == null && root.val == target) {
            return null;
        }
        return root;
    }
}
func removeLeafNodes(root *TreeNode, target int) *TreeNode {
    if root == nil {
        return nil
    }
    root.Left = removeLeafNodes(root.Left, target)
    root.Right = removeLeafNodes(root.Right, target)
    if root.Left == nil && root.Right == nil && root.Val == target {
        return nil
    }
    return root
}

复杂度分析

  • 时间复杂度:$O(n)$。每个节点只进入和退出递归一次,节点上的判断与指针修改都是 $O(1)$。
  • 空间复杂度:$O(h)$。h 是树高,空间来自递归栈;平衡树为 $O(\log n)$,退化链为 $O(n)$。

关键点总结

  • 父节点依赖孩子的最终状态,遍历顺序必须是“左、右、根”的后序。
  • 递归返回“处理后的子树根”,既能表达保留,也能用 null 表达删除;根节点无需额外特判。
  • 必须把递归结果写回左右指针,否则下层删除不会反映到树结构中。
  • 一轮后序遍历等价于题目要求的“不断删除直到稳定”:新产生的目标叶子会在同一次回溯中继续被删除。

易错点总结

  • 先判断再递归:看到的是原始叶子状态。[1,2,null,2] 中上层 2 初次检查时还有孩子,之后不会再被检查,连锁删除失败。
  • 忘记写回递归结果:只调用递归却不执行 root.left = ...,下层即使返回 null,原指针仍然存在。
  • 只判断节点值[2,3]target = 2 中根 2 不是叶子,不能连同非目标孩子一起删除。
  • 遗漏根节点删除[2]target = 2 的正确结果是空树,因此入口必须接收并返回递归得到的新根。
  • 反复整树扫描:结果虽正确,但目标值链会每轮只删一层,最坏退化为 $O(n^2)$;一次后序遍历即可完成。

相似题目

题目 难度 考察点
543. 二叉树的直径 简单 后序返回值只用于统计,不改动树的结构
814. 二叉树剪枝 中等 剪枝条件换成「子树中不含 1」,同一套返回新根范式
1120. 子树的最大平均值 中等 后序需要向上带回和与数量两个聚合量