树节点删除问题的递归解法详解:从链表到树的思维迁移

127 阅读4分钟

这篇文章 同样是在回应艾神提单中的问题:

  1. 在什么情况下,DFS 需要有返回值?什么情况下不需要有返回值?

链表和树的共性

在链表题中,我们常常碰到删除节点的题目。遇到这种题目,我们会使用以下的格式:

curr->next = curr->next->next;

链表的回收进行了俩个动作: 1.对于需要删除节点的prev节点next的更新,防止其指向空虚节点。 2.delete node。节点本身的删除。

同时,树的删除节点也是同样的动作:

  1. 让它的父节点正确地更新指向它的指针,避免 悬空指针(dangling pointer)。
  2. 释放该节点的内存(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;  // 返回当前子树的根
}

经典例题解析

450. 删除二叉搜索树中的节点

删除节点有很多方法,这道题要求你将左节点黏贴到右节点的最左边并返回。按要求做即可。

第一次做小张是一次做出来的全对。以后也要自己一次性做出来哦。

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总结版)

  1. 状态分析

    • 当前节点是否需要删除?
  2. 指针管理

    • 使用返回值更新父节点指针
    • 后序遍历确保子树处理完成
  3. 边界处理

    • 空节点直接返回
    • 根节点的特殊处理
  4. 复杂度控制

    • 时间复杂度:O(n)
    • 空间复杂度:O(h) 递归栈空间

五、常见错误防范

  1. 前序删除陷阱

    // 错误写法:提前删除导致访问非法内存
    void dfs(TreeNode* root) {
        if (shouldDelete(root)) {
            delete root; // 危险操作!
            return;
        }
        dfs(root->left);
        dfs(root->right);
    }
    
  2. 悬空指针风险

    • 未及时置空父节点指针
    • 在删除节点后访问子树

六、总结与提升

  1. 模式识别

    • 树删除问题 ≈ 后序遍历 + 指针更新
    • 递归返回值承载子树结构信息
  2. 思维训练

通过掌握递归模板的本质原理,结合具体问题的特殊条件进行适配修改,即可高效解决各类树节点删除问题。保持对指针操作和递归逻辑的清晰认知,是攻克此类问题的关键。