删除指定值叶子节点时,为什么删除后父节点可能也会变成叶子?
简化版
删除指定值叶子节点要用后序遍历:先递归处理左右子树,再判断当前节点是否变成目标值叶子。因为孩子删掉后,父节点可能从非叶子变成叶子,也需要继续删除。
详细版
这题不能只扫描一次原始叶子。比如父节点值也是 target,它原本有一个 target 叶子孩子;孩子删除后,父节点变成叶子,也应该被删除。
正确思路:
- 递归处理左子树,更新
node.left。 - 递归处理右子树,更新
node.right。 - 如果当前节点左右都为空,并且值等于 target,返回 null。
- 否则返回当前节点。
后序遍历是关键,因为当前节点是否是叶子,要在孩子删除之后才能判断。
完整版教学
一、这题为什么不是普通删除
普通“删除所有值等于 target 的叶子”听起来像找叶子即可。但删除会改变树结构。一个节点原本不是叶子,可能因为孩子被删光而变成新叶子。
例子:
1
/
2
/
2
target = 2
最底下的 2 先删除后,中间的 2 变成叶子,也要删除。最后根 1 保留。
二、为什么必须后序
当前节点是否该删,取决于处理完左右子树后的状态。如果先判断当前节点,再处理孩子,就会错过“删除孩子后变成叶子”的情况。
后序顺序是:
处理左子树
处理右子树
处理当前节点
这个顺序符合依赖关系。父节点要等孩子的最终结果出来,才能判断自己是不是叶子。
三、递归返回值代表什么
递归函数可以返回处理后的子树根。返回 null 表示这棵子树被删掉;返回 node 表示保留。
node.left = remove(node.left, target);
node.right = remove(node.right, target);
if (!node.left && !node.right && node.val === target) return null;
return node;
这段代码的关键是把递归结果重新接回父节点。只调用递归但不赋值,父节点仍然指向旧孩子。
四、为什么根节点也可能被删除
如果整棵树最后只剩一个 target 根节点,根也应该删除。递归返回值天然支持这一点:最终函数返回 null,表示整棵树被删空。
单节点 target
remove(root) -> null
如果你只在父节点里删除孩子,就处理不了根节点,因为根没有父节点。返回新根是树修改题的常用技巧。
五、和普通剪枝题的关系
这题属于树剪枝。剪枝类问题一般都适合后序,因为是否保留当前节点常依赖子树处理结果。
| 问题 | 为什么后序 |
|---|---|
| 删除目标叶子 | 孩子删后父可能变叶子 |
| 删除全 0 子树 | 要知道子树是否全 0 |
| 修剪路径和不足 | 要知道左右路径是否有效 |
一看到“删除后还可能继续影响父节点”,就要想到后序。
六、复杂度和边界
每个节点最多访问一次,时间复杂度 O(n)。递归栈空间和树高有关,最坏退化链是 O(n),平衡树是 O(log n)。
记忆钩子:树剪枝先问“父节点判断是否依赖孩子结果”;如果依赖,就十有八九用后序。
七、常见误区与追问
- 误区:只删除原始叶子就够了。 删除会让父节点变成新叶子,可能还要继续删。
- 误区:先判断当前节点再递归孩子。 当前是否叶子必须在孩子处理后判断。
- 误区:递归结果不用接回去。 必须
node.left = ...,否则删除结果不会反映到父节点。 - 追问:根节点能被删除吗? 能,最终返回 null 即表示整棵树为空。
- 追问:复杂度是多少? 时间 O(n),递归栈空间 O(h)。
八、加强记忆
删除目标叶子的核心不是“找叶子”,而是“删除会产生新叶子”。后序遍历让孩子先完成变化,父节点再根据新状态决定去留。递归返回处理后的根,才能自然覆盖根节点被删的情况。