删除给定值的叶子节点 图解题解
这道题到底在问什么
- 输入
- root=[10,20,30,20,null,20,40], target=20
- 输出
- [10,null,30,null,40]
- 输入
- root=[20,20,20], target=20
- 输出
- [](空树)
最优解:为什么这么做
一句话答案:LeetCode 1325 删除给定值的叶子节点:后序递归先把左右孩子删完再看自己,删掉叶子后父亲可能变新叶子接着连锁删,一趟后序回溯里天然搞定,时间 O(n)、空间 O(h)。
给一棵树和 target,要删掉哪些叶子
给你二叉树 root 和整数 target,把所有值等于 target 的叶子节点删掉。叶子,就是左右都没有孩子的节点。麻烦在删除会传染:删掉一个叶子后,它的父亲可能因为没了孩子变成新叶子,若新叶子值也等于 target 就接着删,直到删不动。题面例子 root=[10,20,30,20,null,20,40]、target=20。
见到值等于 target 就删,会把树删坏
见值等于 target 的节点就删,树很快散架:还挂着孩子的中间节点被删,它底下整棵子树就断链,答案就错了。所以『值等于 target』只是一半条件,另一半是它此刻必须是叶子,只有摘掉叶子才不牵连别人。剩下的问题就一个:判定一个节点时,怎么保证它的孩子已经删干净?
父亲删不删,得等孩子先删完
一个节点会不会变成叶子,取决于它的孩子删光了没有。所以判它之前得先把左右孩子处理完:递归扎进左右子树清干净,最后才回头轮到自己——先孩子、后父亲,这就是后序。等递归从孩子回来时,左右都空了、值又等于 target,这一刻它才是该删的叶子;只要还留着一个孩子,它就不是叶子,值撞上 target 也不动它。倒过来用前序先判父亲,孩子还没删,连锁的新叶子就漏了。
每个节点递归里做三步、返回一个结果
递归函数 removeLeafNodes(root, target) 分三步。边界:节点为空返回空。接着把左右孩子交给递归,注意把返回值接回去——root.left = removeLeafNodes(root.left, target)、root.right 同理,孩子被删时返回的空就接到 root.left 上。最后判自己:root.left 和 root.right 都空、且 root.val 等于 target,返回空把自己删掉,否则返回 root。
拿题面这棵树,从底下的叶子删起
根 10 左孩子 20、右孩子 30;左边的 20 底下还挂着一个 20;右边的 30 底下挂着 20 和 40。后序从 10 先钻左子树:左孩子 20 还有孩子先放着,到最底下的 20,左右皆空、值 20,命中删掉。
回到父亲 20,左孩子刚被置空、右孩子本就空,值又是 20,自己也成叶子被删——这就是连锁,底下删完正好轮到它。左枝向根返回空。右子树:30 的左孩子 20 是叶子且等于 20,删;右孩子 40 是叶子但 40 不等于 20,保留。回到 30 还留着 40、根 10 还留着 30,都不是叶子,保留。结果 [10,null,30,null,40],正是题面输出。
接回返回值这步漏了,连锁就断了
最隐蔽的坑是递归后忘了把返回值接回孩子指针,只写 removeLeafNodes(root.left, target) 不赋回 root.left。孩子删了但父亲指针还指着它,连锁就断了。复杂度上后序每个节点访问一次,时间 O(n);递归栈深度是树高 h,最坏 O(n)、平衡 O(log n),空间 O(h)。[20,20,20]、target=20 会从底下一路连锁删到根,返回空树。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢一句:先把左右孩子递归删完,再看自己。只有自己当前是叶子、且值等于 target 才删。删除是自底向上连锁的,下面每一帧都在套这条规则。
- 4target = 20,三个 20 待处理先看清这棵树。根是 10,它的左孩子是 20、右孩子是 30。左边那个 20 下面还挂着一个 20;右边的 30 下面挂着一个 20 和一个 40。target 等于 20,所以图里三个值为 20 的节点都是潜在的删除对象,但能不能删要看它当时是不是叶子。
- 5叶子 = 左枝底 20 / 右枝 20 / 右枝 40紫色标出来的是这一刻真正的叶子,也就是没有孩子的节点:左枝最底下的 20、右枝下面的 20、还有右枝下面的 40。删除只从叶子开始。中间的 10、20、30 现在都还有孩子,暂时动不了,得等孩子处理完再说。
- 6当前在根 10,先去左子树后序遍历从根 10 出发,但根要不要删,得等它的左右子树全部处理完才知道,所以现在先不判根,顺着左孩子一路往下钻。记住后序的精神:先孩子,后自己。
- 7当前在左枝的 20,继续向下走到根的左孩子,这个 20。它下面还挂着一个 20,所以它现在不是叶子,还不能判。继续往它的左孩子钻下去,把底层先处理掉。
- 8当前在左枝底 20,它没有孩子钻到最底下这个 20。它左右都没有孩子,到头了。这就是一个叶子,可以开始判定它该不该删。
- 9叶子 + 值 20 等于 target判定这个叶子:它确实没有孩子,而且值是 20,正好等于 target。两个条件都满足,该删。下一帧就把它标红删掉。
- 10删除左枝底 20,返回空给父亲左枝最底的 20 被删除,标红。在代码里删除不是真的抹掉内存,而是让它的父亲指向它的那根指针被置成空。这个空值会作为递归结果返回给它的父亲,也就是上面那个 20。
- 11左孩子已置空,右孩子本就空回到上面这个 20。它刚收到孩子返回的空值,左孩子指针断开了。而它的右孩子本来就是空的。于是现在它左右都没有孩子了,情况发生了变化。
- 12左右都空,它现在是叶子,值 20 等于 target关键的一刻来了。这个 20 原本不是叶子,可它的左孩子被删后,它现在左右都空,自己升级成了新叶子。再看它的值,正好也是 20,等于 target。新叶子又命中目标值,按规则它也得删。
- 13连锁:删了底层叶子,父亲也成叶子被删左枝这个 20 被删除,标红。这就是题目说的连锁删除:先删掉底层那个 20,它的父亲因此变成新叶子,而新叶子的值还是 20,于是父亲也跟着被删。一趟后序遍历里,这种连锁是顺着回溯自动发生的。它再向上返回一个空。
- 14根的左子树处理完,转向右子树回到根 10。它的左孩子返回了空,于是根的左孩子指针也断开了,整条左枝就剩两个红色的已删节点。根自己先别急着判,它还有右子树没处理。现在转向右边的 30。
- 15当前在 30,继续向下走到根的右孩子,这个 30。它下面挂着一个 20 和一个 40,有孩子,不是叶子,先不判。按后序规矩,继续往它的左孩子钻。
- 16当前在右枝 20,它没有孩子钻到 30 的左孩子,这个 20。它左右都没有孩子,又是一个叶子,可以判了。
- 17叶子 + 值 20 等于 target判定这个叶子:没有孩子,值是 20,等于 target,两个条件都满足,该删。
- 18删除右枝 20,返回空给 3030 的左孩子这个 20 被删除,标红,向 30 返回空。注意这次和左边不一样:30 的右边还有别的孩子,所以 30 不一定会变成叶子,等右孩子处理完再看。
- 19当前在 40,它没有孩子回到 30,处理它的右孩子,这个 40。它左右都没有孩子,是个叶子,开始判定。
- 20值 40 不等于 20,保留,变绿判定这个 40:它确实是叶子,可是它的值是 40,不等于 target 20。规则要求值等于 target 才删,所以它保留,标绿。它向 30 返回的是它自己,不是空。
- 21左孩子空,右孩子 40 还在,30 有孩子回到 30 做后序判定。它的左孩子被删后是空的,但右孩子 40 保留了下来,所以 30 此刻仍然有孩子,它根本不是叶子。不是叶子就不删,哪怕值碰巧等于 target 也不删,因为删除只针对叶子。30 保留,标绿,向根返回它自己。
- 22左孩子空,右孩子 30 还在,根有孩子最后回到根 10 做后序判定。它的左子树整条被删光了,左孩子是空的,但右孩子 30 保留着,所以根仍然有孩子,不是叶子,保留。再说根的值是 10 也不等于 target。根标绿,整棵树处理完毕。
- 23结果 [10,null,30,null,40]收工。绿色的是保留下来的节点:根 10、它的右孩子 30、以及 30 的右孩子 40。红色的三个 20 全被删掉了。按层序写出来,结果就是 [10,null,30,null,40],根的左孩子位置是空。
- 24左枝底 20 被删 → 左枝 20 变叶被删再回放一下最精彩的连锁:左枝最底的 20 先作为叶子被删,它的父亲那个 20 因此失去全部孩子、升级成新叶子,新叶子值还是 20,于是它也被删。这条自底向上的连锁,正是这道题区别于普通叶子删除的核心,而后序遍历天然就把这件事一次办妥。
⚠️ 容易写错的地方
✗ 错:用前序:先判当前节点再递归孩子
✓ 对:必须后序:先递归孩子,再判当前
父节点能否删取决于孩子删完后它是否变叶子,前序时孩子还没删,连锁的新叶子会漏删
✗ 错:只用「值等于 target」一个条件就删
✓ 对:必须同时满足「是叶子」且「值等于 target」
只看值会把还有孩子的中间节点也删,破坏树结构
✗ 错:递归后忘了把返回值接回孩子指针
✓ 对:写成 root.left = dfs(root.left)、root.right = dfs(root.right)
不接回,父节点看不到孩子已删,既不会断链也无法触发连锁
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def removeLeafNodes(
self, root: Optional[TreeNode], target: int
) -> Optional[TreeNode]:
if root is None:
return None
root.left = self.removeLeafNodes(root.left, target)
root.right = self.removeLeafNodes(root.right, target)
if root.left is None and root.right is None and root.val == target:
return None
return rootC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* removeLeafNodes(TreeNode* root, int target) {
if (!root) {
return nullptr;
}
root->left = removeLeafNodes(root->left, target);
root->right = removeLeafNodes(root->right, target);
if (!root->left && !root->right && root->val == target) {
return nullptr;
}
return root;
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
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;
}
}复杂度
时间
O(n)
n 是节点数。后序遍历每个节点只访问一次,每次只做常数次比较和指针赋值。连锁删除不会重复访问,因为它顺着同一趟回溯自然完成
空间
O(h)
只用递归栈,没有额外容器。栈的峰值深度等于树高 h:链状树最坏 O(n),平衡时 O(log n)。按峰值算空间就是 O(h)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 删除给定值的叶子节点 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么一趟后序就能把连锁处理完,不用反复扫几遍?+
后序本身就是自底向上收的。一个叶子被删、递归返回到父亲时,父亲指向它的指针已经置空,而父亲此刻正好在做自己的后序判定,要是另一个孩子也空了,它当场就成了新叶子、立即接受同样的判定。所以连锁是顺着回溯发生的,一层删完刚好轮到上一层,不用第二趟。
删除一个节点在代码里到底干了什么,是真释放内存吗?+
不是。就是让父亲指向它的那根指针被赋成空——root.left = removeLeafNodes(root.left, target),递归返回空时这个空接到了 root.left 上,孩子就从树里脱了钩。父亲拿到处理后的子树根接回来即可,内存交给语言自己回收。
能不能用层序,也就是一层层横着走的 BFS 来做?+
直接自顶向下的 BFS 不行,删除是自底向上传播的,上层得等下层的结果。硬要用 BFS,得先记下每个节点的父亲和孩子数,从叶子开始像拓扑排序那样反向往上删,写起来明显更绕。后序递归是最干净的写法。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 删除给定值的叶子节点 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。