这篇文章 同样是在回应艾神提单中的问题:
- 在什么情况下,DFS 需要有返回值?什么情况下不需要有返回值?
链表和树的共性
在链表题中,我们常常碰到删除节点的题目。遇到这种题目,我们会使用以下的格式:
curr->next = curr->next->next;
链表的回收进行了俩个动作: 1.对于需要删除节点的prev节点next的更新,防止其指向空虚节点。 2.delete node。节点本身的删除。
同时,树的删除节点也是同样的动作:
- 让它的父节点正确地更新指向它的指针,避免 悬空指针(dangling pointer)。
- 释放该节点的内存(delete root)
所以 我们不能使用不能直接用 void dfs(TreeNode* & root) 删除,而需要 TreeNode* dfs(TreeNode* root) 递归返回新的子树。
树删除问题的递归模板
树删除核心逻辑:
- 通过递归返回值实现父节点指针更新
- 后序遍历确保子树处理完成后再操作当前节点
TreeNode* dfs(TreeNode* root) {
if (!root) return nullptr;
// 递归处理左右子树,并更新当前节点的指针
root->left = dfs(root->left);
root->right = dfs(root->right);
// 如果当前节点需要被删除
if (shouldDelete(root)) {
delete root; // 释放当前节点
return nullptr; // 告诉父节点,这个子树已经被删除
}
return root; // 返回当前子树的根
}
经典例题解析
删除节点有很多方法,这道题要求你将左节点黏贴到右节点的最左边并返回。按要求做即可。
第一次做小张是一次做出来的全对。以后也要自己一次性做出来哦。
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if(!root) return nullptr;
if(root->val == key){
// 1. root 为叶子节点
if(!root->left && !root->right){
return nullptr;
}// 2. root 不是叶子节点
else{
TreeNode* l = deleteNode(root->left, key);
TreeNode* r = deleteNode(root->right, key);
// 单节点 直接往上移
if(r == nullptr) return l;
else if(l == nullptr) return r;
// 双节点 将left节点移到right的最左边
TreeNode* curr = r;
while(curr->left){
curr = curr->left;
}
curr->left = l;
return r;
}
}else{
root->left = deleteNode(root->left, key);
root->right = deleteNode(root->right, key);
}
return root;
}
};
说白了这种方式就是后续遍历。左右节点先更新好,然后treenode 根据左右节点返回的更新值去更新自己的节点。这样确保了treenode左右节点正确更新,防止node 指向虚空指针。
1110. Delete Nodes And Return Forest
class Solution {
vector<TreeNode*> ans;
unordered_set<int> to_d;
void toMap(const vector<int>& to_delete){
to_d.insert(to_delete.begin(), to_delete.end());
}
TreeNode* dfs(TreeNode* root, bool insert){
if(!root) return nullptr;
// 先判断delete这件事情
if(to_d.find(root->val) != to_d.end()){
dfs(root->left, true);
dfs(root->right, true);
return nullptr;
}else{
if(insert){
ans.push_back(root);
}
root->left = dfs(root->left, false);
root->right = dfs(root->right, false);
}
return root;
}
public:
// 其实这题做了俩个动作:删除(更新节点)+ insert推入
vector<TreeNode*> delNodes(TreeNode* root, vector<int>& to_delete) {
if(!root) return {};
toMap(to_delete);
dfs(root, true);
return ans;
}
};
这道题其实是我写这篇文章的原因。写完这篇文章的思考后直接一遍过。下次也要一遍过哦。
通用解题思维框架 (deepseek总结版)
-
状态分析:
- 当前节点是否需要删除?
-
指针管理:
- 使用返回值更新父节点指针
- 后序遍历确保子树处理完成
-
边界处理:
- 空节点直接返回
- 根节点的特殊处理
-
复杂度控制:
- 时间复杂度:O(n)
- 空间复杂度:O(h) 递归栈空间
五、常见错误防范
-
前序删除陷阱:
// 错误写法:提前删除导致访问非法内存 void dfs(TreeNode* root) { if (shouldDelete(root)) { delete root; // 危险操作! return; } dfs(root->left); dfs(root->right); } -
悬空指针风险:
- 未及时置空父节点指针
- 在删除节点后访问子树
六、总结与提升
-
模式识别:
- 树删除问题 ≈ 后序遍历 + 指针更新
- 递归返回值承载子树结构信息
-
思维训练:
-
完成LeetCode相关题目:
-
通过掌握递归模板的本质原理,结合具体问题的特殊条件进行适配修改,即可高效解决各类树节点删除问题。保持对指针操作和递归逻辑的清晰认知,是攻克此类问题的关键。