算法精讲 | 树(三):删除操作の外科手术——像园艺大师一样修剪二叉树

26 阅读5分钟

🌳 算法精讲 | 树(三):删除操作の外科手术——像园艺大师一样修剪二叉树

📅 2025/03/12 || 推荐阅读时间 18分钟


🌟 开篇故事

小明在玩「二叉树养成游戏」时,误删了关键节点导致整棵树瘫痪!后来发现删除节点就像给树做手术:既要精准切除病灶,又要完美缝合伤口!今天我们就来学习这把神奇的手术刀!


🗺️ 知识导航图

graph TD  
    A[删除场景] --> B[三种情况]  
    A --> C[递归技巧]  
    B --> D[叶子节点]  
    B --> E[单子节点]  
    B --> F[双子节点]  
    C --> G[后序遍历]  
    C --> H[返回值设计]  
    F --> I[找后继节点]  
    F --> J[值替换法]  
    E --> K[指针重定向]  

一、删除原理剖析(配图解 + 代码)

1.1 删除手术三原则 🩺

graph LR  
    A[找到目标节点] --> B[判断子节点数量]  
    B -->|0| C[直接切除]  
    B -->|1| D[桥接血管]  
    B -->|2| E[器官移植]  
📜 代码模板(Java版)
public TreeNode deleteNode(TreeNode root, int key) {  
    if (root == null) return null;  
  
    // 🔍 搜索目标节点(BST特性)
    if (key < root.val) {  
        root.left = deleteNode(root.left, key);  
    } else if (key > root.val) {  
        root.right = deleteNode(root.right, key);  
    } else {  
        // 🎯 命中目标!开始手术  
        if (root.left == null) return root.right;      // 右子节点顶替  
        if (root.right == null) return root.left;     // 左子节点顶替  
    
        // 🌟 双子节点:找右子树最小节点移植  
        TreeNode minNode = findMin(root.right);  
        root.val = minNode.val;                       // 🧬 细胞复制  
        root.right = deleteNode(root.right, root.val);// 🪓 切除多余节点  
    }  
    return root;  
}  

private TreeNode findMin(TreeNode node) {  
    while (node.left != null) node = node.left;  
    return node;  
}  
🆚 普通二叉树 vs 二叉搜索树删除对比
特征普通二叉树二叉搜索树
搜索效率O(n)O(h)
删除策略任意替换法严格保持有序性
平衡性维护不需要可能需要旋转
时间复杂度最差 O(n)平均 O(log n)

二、复杂场景处理指南

2.1 四种特殊场景对照表

场景描述易错点解决方案
删除根节点忘记处理空树增加 root==null 判断
重复元素树误删多个相同节点递归时保留原始结构
平衡树(AVL)忘记更新高度添加 height 重计算
带父指针的树父节点引用未更新双向链表式更新

2.2 AVL树删除操作图解

graph BT  
    A((20)) --> B((10))  
    A --> C((30))  
    C --> D((25))  
    C --> E((40))  
  
    %% 删除30节点后触发左旋
    style C fill:#fbb  
    E --> D  
    A -.-> E  
⚖️ AVL树删除代码片段
private TreeNode balance(TreeNode node) {
    // 更新高度
    updateHeight(node);
  
    int balanceFactor = getBalanceFactor(node);
    // 左子树高 -> 右旋
    if (balanceFactor > 1) {
        if (getBalanceFactor(node.left) >= 0) {
            return rotateRight(node);
        } else {
            node.left = rotateLeft(node.left);
            return rotateRight(node);
        }
    }
    // 右子树高 -> 左旋
    if (balanceFactor < -1) {
        if (getBalanceFactor(node.right) <= 0) {
            return rotateLeft(node);
        } else {
            node.right = rotateRight(node.right);
            return rotateLeft(node);
        }
    }
    return node;
}

三、高频面试题精讲

3.1 LeetCode 450. 删除节点 🔗

解法一:递归法(优雅の手术刀)
// 🎻 小提琴般优雅的递归实现
public TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;
  
    if (key < root.val) {
        root.left = deleteNode(root.left, key); // 🪢 左子树缝合
    } else if (key > root.val) {
        root.right = deleteNode(root.right, key); // 🪢 右子树缝合
    } else {
        if (root.left == null) return root.right; // 🚑 单子节点急救
        if (root.right == null) return root.left;
  
        TreeNode successor = root.right; // 🕵️ 找器官捐献者
        while (successor.left != null) successor = successor.left;
        root.val = successor.val; // 🧬 基因移植
        root.right = deleteNode(root.right, successor.val); // 🔪 切除冗余
    }
    return root;
}
解法二:迭代法(工业级の手术台)
// 🏭 工业级精密迭代实现
public TreeNode deleteNodeIterative(TreeNode root, int key) {
    TreeNode cur = root, pre = null;
    while (cur != null && cur.val != key) {
        pre = cur;
        cur = key < cur.val ? cur.left : cur.right;
    }
    if (cur == null) return root;
  
    if (cur.left != null && cur.right != null) { // 🎭 双子节点面具
        TreeNode successorParent = cur;
        TreeNode successor = cur.right;
        while (successor.left != null) {
            successorParent = successor;
            successor = successor.left;
        }
        cur.val = successor.val;
        cur = successor; // 🎯 锁定真实目标
        pre = successorParent;
    }
  
    TreeNode child = cur.left != null ? cur.left : cur.right;
    if (pre == null) return child; // 🏴‍☠️ 根节点海盗船
    if (pre.left == cur) pre.left = child;
    else pre.right = child;
  
    return root;
}
复杂度分析表
方法类型时间复杂度空间复杂度适用场景
递归法O(h)O(h)树高度较低时
迭代法O(h)O(1)避免栈溢出
平衡树O(log n)O(log n)频繁增删场景

3.2 剑指 Offer 193. 二叉搜索树的最近公共祖先 🔗

graph TB
    A((6)) --> B((2))
    A --> C((8))
    B --> D((0))
    B --> E((4))
    C --> F((7))
    C --> G((9))
    E --> H((3))
    E --> I((5))
  
    classDef highlight fill:#f9f,stroke:#333;
    class A highlight
  
    style B fill:#f96
    style C fill:#f96
    linkStyle 0 stroke:#f00,stroke-width:2px
    linkStyle 1 stroke:#0f0,stroke-width:2px
  
    %% 注释
    T[示例树结构]:::highlight
    P(p=2):::highlight
    Q(q=8):::highlight
    LCA(6):::highlight
问题分析

给定二叉搜索树(BST)和两个节点p、q,需找到它们的最近公共祖先。BST特性:左子树所有节点值 < 根节点值 < 右子树所有节点值。

解题思路
方法一:递归法(时间复杂度O(n))
  1. 若当前节点值 > p和q的值,向左子树搜索
  2. 若当前节点值 < p和q的值,向右子树搜索
  3. 否则当前节点即为LCA
方法二:迭代法(时间复杂度O(n))
  1. 循环遍历树结构
  2. 根据节点值比较决定移动方向
  3. 找到第一个分叉点即停止
代码实现
class Solution {
    // 递归实现
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root.val > p.val && root.val > q.val) {
            return lowestCommonAncestor(root.left, p, q);
        } else if (root.val < p.val && root.val < q.val) {
            return lowestCommonAncestor(root.right, p, q);
        }
        return root;
    }

    // 迭代实现
    public TreeNode lowestCommonAncestorIterative(TreeNode root, TreeNode p, TreeNode q) {
        while (true) {
            if (root.val > p.val && root.val > q.val) {
                root = root.left;
            } else if (root.val < p.val && root.val < q.val) {
                root = root.right;
            } else {
                return root;
            }
        }
    }
}

四、避坑指南

删除操作的六大陷阱

1️⃣ 内存泄漏:C++选手忘记释放节点内存

2️⃣ 高度失衡:删除后忘记检查平衡因子

3️⃣ 指针悬空:未及时更新父节点引用

4️⃣ 最小值误删:在右子树找最小值时漏判空

5️⃣ 递归栈溢出:处理超大树时未优化

6️⃣ 并发修改:多线程环境下非原子操作

🚨 易错代码示例
// 错误!未处理父节点引用
void deleteNode(Node root, int key) {
    // ...找到节点后直接置空
    node = null; // ❌ 父节点仍然指向该位置!
}

五、课后训练场

5.1 基础关

5.2 进阶关

5.3 地狱关


六、灵魂拷问室 💬

面试官:如何证明删除后的树仍然是BST?

:中序遍历结果保持严格递增序即为合法BST

面试官:如何处理重复元素的删除?

:方案一:仅删除第一个遇到的节点;方案二:设计支持重复值的BST结构

面试官:删除操作会影响哪些树属性?

:高度、平衡因子、父指针、子树大小(若维护)


七、下期预告

《树的旋转操作——像体操运动员一样玩转平衡》 🚀 亮点剧透:

  • 🤸 AVL树的四种旋转姿势
  • ⚖️ 红黑树的颜色翻转魔术
  • 🧮 平衡因子计算秘籍
graph RL  
    A((不平衡树)) --> B{旋转手术}  
    B -->|左旋| C((平衡树))  
    B -->|右旋| D((平衡树))  

🌟 算法留言墙:你曾在删除操作中踩过哪些坑?在评论区分享你的惊险故事吧!

pie
    title 删除操作错误类型统计
    "指针未更新" : 38
    "平衡性破坏" : 25
    "内存泄漏" : 18
    "递归溢出" : 12
    "其他" : 7