814. 二叉树剪枝(Binary Tree Pruning)

难度:中等 | 主题:二叉树、递归、后序遍历

题目

给你二叉树的根节点 root,每个节点值都是 0 或 1。请剪去所有节点值都为 0 的子树(即不含 1 的子树),返回剪枝后的树根。

示例

 
输入:root = [1,null,0,0,1]
 
输出:[1,null,0,null,1]
 
解释:
 
     1                 1
 
      \       →         \
 
       0                 0
 
      / \                 \
 
     0   1                 1
 

思路

先讲个故事:修剪枯枝

想象一棵盆栽,绿色枝条代表 1,枯黄枝条代表 0。你想剪掉所有完全没有绿叶的分支。

你从树梢开始检查:

  1. 先看最末端的细枝——如果全是枯黄(0),咔嚓剪掉

  2. 再看上一级——如果左右两边的细枝都被剪光了,而且这根也枯黄,那它也要剪掉

  3. 不断往上推,直到根

这不就是后序遍历吗?——先处理子树,再决定当前节点。


引导式推导:自底向上做决定

核心规则:一个节点该不该剪?

当且仅当:节点值 = 0 左右子树都被剪没了(都是空)

因为:

  • 节点值 = 1 → 子树含 1,怎么都不能剪

  • 左右子树有 1 → 子树含 1,不能剪

  • 左右子树还有值 → 子树还有节点,等它们被处理后再说

为什么必须是后序?

 
前序:先看父节点 → 不知道子树有没有 1,无法决定
 
中序:先看左子树 → 看了父节点才看右子树,也是错
 
后序:先看子树 → 子树的结果决定了当前节点要不要留
 

后序的核心思想:子问题先解决,父问题的答案依赖子问题的结果。


递归自顶向下的视角

 
pruneTree(root):
 
  1. 递归剪左子树 → root.left = 结果
 
  2. 递归剪右子树 → root.right = 结果
 
  3. 如果 root.val == 0 且左右子树都被剪空了:
 
     返回 None(剪掉自己)
 
     否则返回 root(保留)
 

每一步的返回值就是”剪枝后这棵子树长什么样”。

父节点直接接住返回值就完成了剪枝——接住即剪掉


代码

 
def pruneTree(self, root):
 
    # 空节点:不需要处理
 
    if not root:
 
        return None
 
    # 后序:先递归剪掉左右子树中不含 1 的分支
 
    root.left = self.pruneTree(root.left)
 
    root.right = self.pruneTree(root.right)
 
    # 后序位置:处理完子树后判断当前节点
 
    # 剪枝条件:自己是 0 且左右都被剪空了 → 以自己为根的子树不含 1
 
    if root.val == 0 and not root.left and not root.right:
 
        return None      # 剪掉
 
    return root          # 保留
 

为什么 root.left = self.pruneTree(root.left) 能自动完成剪枝?

因为 pruneTree 返回剪枝后的子树根。如果左子树全被剪了,返回 None,root.left = None。如果左子树还有东西,返回左子树的根,root.left = 原来的左子树根。一句赋值同时实现了”剪掉”和”保留”两种效果。


复杂度

指标解释
时间O(n)每个节点访问一次
空间O(h)递归栈深度,最坏 O(n)(链),平均 O(log n)

实战考量

频率分析

出现在:字节/美团/百度 二叉树后序应用题,低频但典型。考察”后序思维”——你能不能意识到必须先知道子树的情况才能决定当前节点。

延伸思考

Q:为什么不能用前序遍历?

A:前序是先处理当前节点再递归子树。但剪枝决策依赖子树结果——你不知道左右子树还有没有 1,就无法判断当前节点是否该剪。前序硬写的话需要额外传递状态信息,逻辑会复杂很多。

Q:剪枝策略的数学归纳法证明?

A:

  • 基础:叶节点为 0 → 被剪掉(返回 None)

  • 归纳:非叶节点,左右子树都返回 None 且自身为 0 → 被剪掉

  • 结论:剪枝后,树中任意子树至少包含一个值为 1 的节点

Q:迭代版怎么做?

A:用栈做后序遍历。需要标记节点访问状态(第一次访问 vs 处理完左右子树)。处理逻辑和递归一样,但要手动维护栈和状态。

Q:如果要求剪掉所有值为 0 的节点(不管子树有没有 1)?

A:那就是”删除所有值为 0 的节点”,不是剪枝。同样是后序,但剪枝条件变成 root.val == 0 就删,不判断左右子树。但要注意删掉后父节点的左/右指针要更新。

Q:如果节点值可以是任何整数,剪掉所有和为 0 的子树?

A:同样后序框架。pruneTree 返回 (剪枝后的子树, 子树和)。(子树和, 剪枝后左右子树) → 如果子树和为 0 且当前节点也被剪后子树和为 0 → 剪掉。

易错点

  • 必须后序,前序/中序不行

  • 剪枝条件 = val == 0 左右子树都为空

  • root.left = self.pruneTree(root.left) 要接住返回值,否则剪了等于没剪

  • 空节点直接返回 None,不进入剪枝判断


生活类比

剪枝 → 修剪盆栽

后序剪枝就像从树梢开始检查:先看最细的枝条(叶节点),枯了就剪;

回到上一级,如果两边细枝都剪了,这根粗枝也枯了,那就整根剪掉。

一直剪到主干——如果整棵树都是枯的(全是 0),最后返回 None 表示”这盆花没救了,扔掉”。


相关题目

题目关系
337打家劫舍III同是后序遍历,不过加 DP
104二叉树的最大深度后序基础框架
110平衡二叉树后序 + 高度判断,同框架

→ 返回题单:LeetCode学习路线图 > 五、二叉树

速记卡(面试闪卡)

Q1:一句话讲清「814. 二叉树剪枝(Binary Tree Pruning)」到底是什么?

A:剪掉所有不含 1 的零值子树,后序遍历、自己是 0 且左右皆空才删。

Q2:题目理解 —— 什么算该剪的枝? —— 怎么理解?

A:像修剪盆栽:只剪完全没绿叶(不含 1)的枯枝。含 1 的再小也要留。英文:Binary Tree Pruning。

Q3:核心思路 —— 为什么必须后序? —— 怎么理解?

A:像从树梢往上查:先看细枝枯没枯,再决定粗枝。父节点留不留取决于子树结果,所以先处理子树。英文:Post-order Decision。

Q4:代码实现 —— 一句赋值怎么剪? —— 怎么理解?

A:像接住下属交回的盆栽:root.left = prune(左) 直接接返回值,返回 None 即剪掉、返回根即保留。英文:Return-as-Cut。

Q5:复杂度与实战 —— 前序为何不行? —— 怎么理解?

A:像没看完子树就下刀:前序不知左右还有没有 1,无法决策。时间 O(n)、空间 O(h)。英文:Top-down Fail。

Q6:核心速记主线有哪些?

  • 剪枝条件:节点值=0 且左右子树都为空才删

  • 必须后序:先递归处理左右,再决定当前节点

  • 用 root.left = prune(root.left) 接住返回值即完成剪/留

  • 前序/中序不行,决策依赖子树结果

  • 空节点直接返回 None,不进入判断

口诀

A:枯枝含一不须剪,

后序自底定去留;

接住返回值即断,

空树归还无一筹。

相关链接