Leetcode 1325. Delete Leaves With a Given Value (二叉树后序遍历经典题)

文章描述了解决删除二叉树中所有值为目标值的叶子节点的问题,提供了两种方法,一种是使用后序遍历和辅助函数,另一种是递归处理。解决方案确保在删除过程中,父节点值也为目标值时也进行删除。
  1. Delete Leaves With a Given Value
    Solved
    Medium
    Topics
    Companies
    Hint
    Given a binary tree root and an integer target, delete all the leaf nodes with value target.

Note that once you delete a leaf node with value target, if its parent node becomes a leaf node and has the value target, it should also be deleted (you need to continue doing that until you cannot).

Example 1:

Input: root = [1,2,3,2,null,2,4], target = 2
Output: [1,null,3,null,4]
Explanation: Leaf nodes in green with value (target = 2) are removed (Picture in left).
After removing, new nodes become leaf nodes with value (target = 2) (Picture in center).
Example 2:

Input: root = [1,3,3,3,2], target = 3
Output: [1,3,null,null,2]
Example 3:

Input: root = [1,2,null,2,null,2], target = 2
Output: [1]
Explanation: Leaf nodes in green with value (target = 2) are removed at each step.

Constraints:

The number of nodes in the tree is in the range [1, 3000].
1 <= Node.val, target <= 1000

解法1:后序遍历!
注意:root = helper(root, target); 不能直接写成helper(root, target); 因为有可能root也被改成NULL。
参考case [1, 1, 1],这里的root也被清掉了。

/**
 * 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) {
        root = helper(root, target); //注意不能直接写成helper(root, target); 因为有可能root也被改成NULL。参考case [1, 1, 1]
        return root;
    }
private:
    TreeNode *helper(TreeNode *root, int target) {
        if (!root) return NULL;
        TreeNode *left = helper(root->left, target);
        TreeNode *right = helper(root->right, target);
        
        if (!left && !right && root->val == target) return NULL;
        root->left = left;
        root->right = right;
        return root;
    }
};

也可以直接写成:

class Solution {
public:
    TreeNode* removeLeafNodes(TreeNode* root, int target) {
        if (!root) return NULL;
        TreeNode *left = removeLeafNodes(root->left, target);
        TreeNode *right = removeLeafNodes(root->right, target);
        
        if (!left && !right && root->val == target) return NULL;
        root->left = left;
        root->right = right;
        return root;
    }
};
<think> 我们正在解决LeetCode 1110:删点成林(Delete Nodes And Return Forest)。目要求删除二叉树中所有节点值在to_delete列表中的节点,然后返回删除后形成的森林(即多个树的根节点组成的列表)。 原代码使用后序遍历(DFS)来实现。下面我将详细解释为什么使用后序遍历,以及是否可以使用中序遍历。 ### 1. 为什么使用后序遍历后序遍历的顺序是:左子树 → 右子树 → 根节点。这种遍历顺序在本中非常关键,原因如下: - **处理子节点后才能确定当前节点的状态**:当我们删除一个节点时,需要将其左右子树(如果存在)作为新的树加入到结果列表中。由于删除操作会断开当前节点与其子节点的连接,因此我们需要先处理子节点,再处理当前节点。这样,在删除当前节点之前,我们已经得到了处理后的左右子树(即已经删除了其中需要删除的节点),然后再决定是否将左右子树加入结果集。 - **避免父节点影响**:如果我们使用前序遍历(根节点 → 左子树 → 右子树),那么我们在处理当前节点时,如果当前节点需要删除,那么我们会将其左右子树加入结果集,然后递归处理左右子树。但是,在递归处理左右子树时,可能会遇到子树中的节点也需要删除,这样就会导致重复加入(因为先加入了当前节点的左右子树,而子树中如果有节点被删除,那么子树本身也会被断开,形成更小的子树)。而后序遍历则避免了这个问,因为我们先处理了左右子树,确保了左右子树内部已经完成了删除操作,然后再处理当前节点。这样,当前节点的左右子树已经是删除后的状态(如果子树中有节点被删除,那么子树会被断开,返回的可能是一个更小的子树或者None)。这样在加入左右子树时,加入的已经是独立的树(不会再被断开)。 - **自底向上**:后序遍历是自底向上的,即从叶子节点开始处理,逐步向上。这样,当我们处理一个节点时,它的子节点已经被正确处理过了(删除或者保留),我们可以安全地修改子节点的指针。 ### 2. 原代码中后序遍历的关键步骤: ```python def dfs(node): if not node: return None # 先递归处理左右子树 node.left = dfs(node.left) # 左子树处理完,返回的是删除操作后的左子树根(可能为None) node.right = dfs(node.right) # 同上 # 处理当前节点 if node.val not in s: # 如果当前节点不需要删除,则返回当前节点 return node # 如果当前节点需要删除,则将其左右子树(如果存在)加入结果集 if node.left: ans.append(node.left) if node.right: ans.append(node.right) return None # 删除当前节点,返回None给父节点 ``` ### 3. 能否使用中序遍历? 中序遍历的顺序是:左子树 → 根节点 → 右子树。 如果我们尝试使用中序遍历,会遇到以下问: - **顺序问**:当我们处理根节点时,我们已经处理了左子树,但还没有处理右子树。如果我们在处理根节点时删除了根节点,那么我们会将左子树(已经处理过删除)加入结果集,并将右子树(还未处理)加入结果集。然而,右子树中可能还有需要删除的节点,这些节点应该在后续处理中被删除,并且它们的子树也应该被加入结果集。但是,由于我们在处理根节点时就将右子树加入了结果集,那么右子树中需要删除的节点在后续处理中也会被处理(删除),导致这些节点被删除后,其子树也会被加入结果集,这样就会出现重复加入(同一个子树被加入了两次:一次是作为根节点的右子树加入,一次是作为右子树中被删除节点的子树加入)。 - **破坏结构**:在删除根节点之前,我们处理了左子树,但处理根节点后,我们切断了根节点与左右子树的连接,然后再处理右子树?这会导致右子树的处理变得复杂,因为原本的父节点已经断开。 因此,使用中序遍历会导致逻辑混乱,难以保证正确性。实际上,本需要先处理子节点(左右子树)再处理父节点(根节点),所以后序遍历是最自然的选择。 ### 4. 其他遍历方式? - **前序遍历**:如前所述,前序遍历在处理当前节点时,如果当前节点被删除,我们会将左右子树加入结果集,然后再递归处理左右子树。但是递归处理左右子树时,如果左右子树中也有节点被删除,那么这些子树又会被加入结果集。这样,同一个子树可能会被多次加入(因为先加入了整个子树,然后子树中删除节点时又会加入子树的一部分)。因此,前序遍历需要额外的机制来避免重复加入,这会使问复杂化。 ### 结论 在本中,使用后序遍历是最合适的,因为它能够保证在处理当前节点之前,其左右子树已经完成了删除操作,这样在删除当前节点时,可以直接将左右子树(已经是独立的树)加入结果集。而中序遍历和前序遍历都会带来逻辑上的问,因此不推荐使用。 ### 相关问 §§相关问§§ 1.二叉树中,后序遍历通常用于解决哪些类型的问? 2. LeetCode上还有哪些经典需要使用后序遍历? 3. 如何修改代码以处理节点值重复的二叉树? 4. 能否用BFS替代DFS实现该算法?优缺点是什么? </think> ### 后序遍历LeetCode 1110 中的关键作用 在解决 LeetCode 1110 (删点成林)的代码中,使用后序遍历是精心设计的核心策略,主要原因如下: #### 📌 使用后序遍历的必然性 1. **自底向上的处理逻辑**(关键原因) - 后序遍历顺序:$\text{左子树} \rightarrow \text{右子树} \rightarrow \text{根节点}$ - 必须**先处理子节点再处理父节点**:当删除节点时,需要确保其子节点已处理完成,才能安全分离子树。例如: ```python node.left = dfs(node.left) # 先处理左子树 node.right = dfs(node.right) # 再处理右子树 ``` - 如果父节点被删除(`node.val in s`),其左右子树需要作为新树加入结果集(`ans.append`) 2. **子树状态依赖** - 父节点的操作(删除/保留)**依赖于子树的处理结果** - 后序遍历保证:当处理节点 $N$ 时,其左右子树已完成所有删除操作并返回最终状态 3. **指针安全更新** - 通过递归返回值更新指针: ```python return node # 保留当前节点 return None # 删除当前节点(父节点对应指针置空) ``` - 前序/中序遍历会导致未处理的子树指针被错误修改 #### ⚠️ 为什么不能用中序遍历? 中序遍历(左→根→右)会导致**结构性错误**: 1. **处理顺序矛盾** - 当处理根节点时,右子树尚未处理 - 若删除根节点并尝试将右子树加入 `ans`: ```python if node.right: ans.append(node.right) # 错误!右子树可能包含需删除节点 ``` - 右子树中的待删除节点尚未处理,加入后会产生脏数据 2. **指针更新失效** - 中序代码示意: ```python node.left = dfs(node.left) # ✅ 左子树处理完成 # 此时若删除当前节点并操作右子树 → 错误! node.right = dfs(node.right) # ❌ 右子树应在删除前处理 ``` 3. **森林完整性破坏** - 可能加入未完全处理的子树 - 已删除节点的子树可能被遗漏(未加入 `ans`) #### 💡 后序遍历正确性验证 通过示例说明: ``` 树结构: 1 / \ 2 3 / \ 4 5 to_delete = [2,3] ``` 后序遍历执行流程: 1. 处理节点4 → 保留 → 返回4 2. 处理节点5 → 保留 → 返回5 3. 处理节点2 → 需删除 → 将左子树4、右子树5加入 `ans` 4. 处理节点3 → 需删除 → 无子树可加 5. 处理节点1 → 保留 → 返回1并加入 `ans` ✅ 最终森林:`[1,4,5]` #### ️ 替代方案可行性 虽然严格意义上的中序遍历不可行,但可通过修改实现类后序效果: ```python # 变通方案(不推荐) def inorder(node): if not node: return left = inorder(node.left) # 先缓存左子树 # 此处模拟后序:在"根"操作前先处理右子树 right = inorder(node.right) if node.val in s: if left: ans.append(left) if right: ans.append(right) return None node.left, node.right = left, right return node ``` 👉 本质上仍是**后序遍历的变形**,实际复杂度与可读性劣于标准后序实现。 ### 结论 后序遍历是解决此类**子树分离问**的最优方法,因其: 1. 天然满足 **自底向上处理** 的需求 2. 确保子树状态在父节点操作前**完全确定** 3. 通过递归返回值实现**安全指针更新** 该模式也适用于类似问(如:[1325. 删除给定值的叶子节点](https://leetcode.cn/problems/delete-leaves-with-a-given-value/))[^1]。 --- ### 相关问
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值