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

/**
 * 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==nullptr)//判断是否整棵树为空
        {
            return nullptr;
        }
        root->left=removeLeafNodes(root->left,target);//先删除左边
        root->right=removeLeafNodes(root->right,target);//再删除右边
        if(!root->left&&!root->right&&root->val==target)//在回溯的时候删除
        {
            return nullptr;//如果某一个节点是叶子节点而且还是值等于target的,那么直接删除
        }
        return root;
    }
};
<think> 我们正在解析LeetCode 1110题的Python代码。题目是:删点成林。给定一棵二叉树的根节点root和一个整数数组to_delete,其中包含一些,要求删除所有在to_delete中的节点。删除后,返回一个森林(即多棵树的集合)。原始树中的每个节点都是唯一的。 代码解析: 这个解决方案使用后序遍历(DFS)递归地处理每个节点。主要思路是: 1. 将to_delete转换为集合s,以便快速检查。 2. 定义一个列表ans,用于存储删除后剩余的树的根节点。 3. 定义一个递归函数dfs(node): - 如果节点为空,返回None。 - 递归处理左子树和右子树(后序遍历)。 - 如果当前节点的删除集合s中: * 那么我们需要删除当前节点。但是,删除前,如果该节点有左子树,则左子树将成为新的树(加入ans),同样处理右子树。 * 然后返回None,表示当前节点被删除(即告诉父节点,这个子节点被删除,因此父节点对应的子指针应置为None)。 - 如果当前节点的不在删除集合中,则返回当前节点(表示保留这个节点,同时因为它已经递归处理了左右子树,所以左右子树的删除操作已经完成,并更新了左右指针)。 4. 最后,处理根节点:如果根节点没有被删除(即dfs(root)返回的不是None,那么根节点所在的树也应该加入ans)。 5. 返回ans。 需要注意的是,在删除一个节点时,我们先将它的左右子树(如果存在)加入ans,然后返回None。由于我们使用后序遍历,所以从底向上处理,这样在删除一个节点时,它的左右子树已经被处理过了(即已经删除了其中需要删除的节点,并且已经将断开的子树加入ans)。 现在,我们逐步分析代码: 1. ans = []:用于存储结果森林中的树根。 2. s = set(to_delete):将删除列表转为集合,提高查询效率。 3. 定义dfs函数: def dfs(node): if node is None: return None # 空节点直接返回None # 递归处理左右子树,并更新当前节点的左右指针(因为子树可能被删除,所以左右子节点可能变为None或新的节点) node.left = dfs(node.left) 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给父节点,表示这个位置被置空 4. 调用dfs(root)后,如果根节点没有被删除,那么整棵树(已删除部分节点)的根节点还在,所以需要加入ans: if dfs(root): ans.append(root) 5. 返回ans。 但是,这里有一个细节:在删除节点时,我们先将左右子树加入ans,然后才返回None。这里加入ans的是被删除节点的左右孩子,这些孩子节点本身并没有被删除(除非它们也在删除集合中,但由于我们是从底向上处理的,所以它们已经经过了递归处理,如果它们需要删除,那么在它们的递归中已经被删除,并且它们的左右子树(如果有)也已经被加入ans,并且它们本身返回None,所以当前节点的左右指针可能已经被更新)。 但是,这里有一个关键点:我们在当前节点删除时,将当前节点的左右子树加入ans。注意,当前节点的左右子树已经被递归处理过,即子树中需要删除的节点已经被删除,并且子树中因为删除而断开的树已经在递归中加入了ans吗?实际上,并不是这样。 我们需要理解:在递归函数中,我们首先递归处理左右子树,然后检查当前节点。当我们处理当前节点时,左右子树已经被处理(即子树中需要删除的节点已经被删除,并且子树中断开形成的树已经加入ans)。然后,如果当前节点需要删除,那么我们将当前节点的左右子树(注意,此时左右子树已经是处理后的子树,并且它们的根节点并没有被删除)加入到ans。这是因为当前节点被删除后,它的左右子树就会独立成为新的树。 但是,这里有一个问题:当前节点的左右子树可能已经被删除(即它们的根节点在删除集合中)?注意,在递归处理左右子树时,如果左右子树的根节点在删除集合中,那么递归函数返回的是None(表示该节点被删除),并且在这个递归中,已经将左右子树根节点的左右子树(如果有)加入ans了。因此,当前节点的左右指针可能已经被更新为None(如果左右子树根节点被删除的话)。所以,在检查当前节点的左右子树是否非空时,实际上检查的是处理后的子树(即如果子树根被删除,则左右指针已经变成了None,或者子树根没有被删除,则返回子树根节点)。 因此,在dfs(node)中: node.left = dfs(node.left) # 处理左子树,如果左子树根被删除,则node.left变为None;否则保留左子树根。 node.right = dfs(node.right) # 同理 然后,如果当前节点需要删除: 那么我们就将当前节点的左子树(如果非空)加入ans,右子树(如果非空)加入ans。 为什么非空?因为如果左子树在处理后变为空(即整个左子树都被删除),那么就不需要加入。 最后,注意根节点需要单独处理:如果根节点没有被删除,那么整棵树(处理后的)需要加入ans。 但是,这里有一个问题:当我们删除一个节点时,我们将其左右子树加入ans,但是这些左右子树可能包含需要删除的节点吗?其实不会,因为我们是从下往上处理的,所以一个节点在删除之前,它的子树已经处理过(删除过)。所以,加入ans的子树根节点一定是不需要被删除的(因为如果子树根需要被删除,那么在递归处理该子树根时,我们已经将它删除,并返回None,并且它的左右子树已经被加入ans,然后它的父节点(当前节点)的左/右指针就会变成None)。因此,被加入ans的节点都是不需要删除的节点。 但是,这里有一个特殊情况:根节点。根节点可能不需要删除,那么整棵树作为一个整体加入ans。 然而,我们需要注意的是,我们在递归过程中,只将被删除节点的左右子树加入ans。而根节点如果不需要删除,我们最后加入。这样,所有不需要删除的节点,要么被包含在某个被删除节点的左右子树中(作为独立子树加入ans),要么就是整棵树的根(如果根没有被删除)。 因此,这个算法正确。 举个例子: 1 / \ 2 3 / \ / \ 4 5 6 7 to_delete = [3,5] 处理过程(后序): 先处理节点4:不需要删除,返回节点4。节点5:需要删除,但是节点5的左右子树为空,所以直接返回None(同时将左右子树(空)不加入ans)。然后节点2:左子树返回节点4,右子树返回None(因为5被删除),节点2不需要删除,所以返回节点2(此时节点2的左指针指向4,右指针为None)。 处理节点6:不需要删除,返回6;节点7:不需要删除,返回7。节点3:需要删除,所以将节点3的左右子树(即6和7)加入ans(此时ans=[6,7]),然后返回None。 处理节点1:左子树返回节点2(即1的左指针指向2),右子树返回None(因为3被删除了)。节点1不需要删除,所以最后将节点1加入ans(此时ans=[6,7,1])。 最终森林:包含三棵树:以1为根(树为:1->2, 2的左子树是4),以6为根,以7为根。 但是注意:原本节点3被删除后,它的左右子树6和7成为独立的两棵树。节点1没有被删除,所以以1为根的树保留。节点5被删除,但它没有子树,所以没有新树产生。 因此,结果正确。 然而,我们还需要注意:在删除节点时,我们只将其左右子树加入ans,而没有考虑该节点本身是否有父节点。由于我们是从下往上处理,当我们处理到父节点时,我们已经将子节点处理过了,所以父节点的左右指针已经被更新(如果子节点被删除,则指针变为None)。 因此,代码逻辑清晰正确。 下面,我们按照要求,将代码中的关键部分用行内数学表达式和独立公式表示(实际上,这个代码解析主要是文字描述,数学表达式较少,但我们可以适当使用)。 注意:题目要求行内数学表达式用$...$,独立公式用$$...$$。但在这个解析中,主要是代码和文字,数学表达式可能用于描述树结构或集合等,但并不是必须的。这里我们尽量在需要时使用。 例如:删除集合$s$,节点$val$等。 由于题目要求生成相关问题,我们在最后生成。 开始写回答: </think> ### LeetCode 1110题:删点成林代码解析 本题要求删除二叉树中所有在 `to_delete` 列表中的节点,并返回删除后形成的森林(多个独立子树的根节点列表)。以下是对给定Python代码的逐步解析: #### 核心思路 1. **后序遍历(DFS)**:从叶子节点向根节点处理,确保子节点处理完毕后再处理父节点。 2. **删除标记**:用集合 `s` 存储待删除,实现$O(1)$查询。 3. **森林生成**:当节点被删除时,其非空左右子树会成为新树的根。 #### 代码逐行解析 ```python class Solution: def delNodes(self, root: Optional[TreeNode], to_delete: List[int]) -> List[TreeNode]: ans = [] # 存储结果的森林列表 s = set(to_delete) # 转换为集合$s$,提高查询效率 def dfs(node: Optional[TreeNode]) -> Optional[TreeNode]: if node is None: return None # 终止条件:空节点直接返回 # 递归处理左右子树(后序遍历) node.left = dfs(node.left) # $\gets$ 更新左子树 node.right = dfs(node.right) # $\gets$ 更新右子树 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) # 处理根节点 if dfs(root): ans.append(root) # 根节点保留则加入森林 return ans ``` #### 关键步骤说明 1. **后序遍历递归**(`dfs`函数): - **叶子节点**:直接检查是否删除(无子树需分离)。 - **非叶子节点**: - 先递归处理左右子树(更新子树结构)。 - 若当前节点在$s$中,则将其左右子树加入`ans`(成为新树)。 - 返回`None`给父节点,实现删除(父节点对应指针置空)。 2. **根节点特殊处理**: - 递归结束后,若根节点未被删除(`dfs(root) is not None`),则将其加入`ans`。 #### 示例分析 假设二叉树: ``` 1 / \ 2 3 / \ 4 5 ``` `to_delete = [2, 3]`,执行过程: 1. 后序遍历访问顺序:4 → 5 → 2 → 3 → 1。 2. 删除节点2:将其左子树(4)、右子树(5)加入`ans`。 3. 删除节点3:将其左右子树(无)加入`ans`(无操作)。 4. 根节点1未被删除,加入`ans`。 5. 最终森林:`[[1], [4], [5]]`(三个独立子树)。 #### 算法复杂度 - **时间复杂度**:$O(n)$,每个节点访问一次。 - **空间复杂度**:$O(n)$,递归栈空间和集合$s$存储。 此解法通过后序遍历和集合查询,高效处理节点删除与森林生成[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值