🌳 算法精讲 | 树(三):删除操作の外科手术——像园艺大师一样修剪二叉树
📅 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))
- 若当前节点值 > p和q的值,向左子树搜索
- 若当前节点值 < p和q的值,向右子树搜索
- 否则当前节点即为LCA
方法二:迭代法(时间复杂度O(n))
- 循环遍历树结构
- 根据节点值比较决定移动方向
- 找到第一个分叉点即停止
代码实现
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 基础关
- 🔗 450.删除二叉搜索树节点 技巧提示:先写递归版再挑战迭代版
5.2 进阶关
- 🔗 1382.将二叉搜索树变平衡 秘籍:中序遍历+重构,删除冗余节点
5.3 地狱关
- 🔗 1676.完全二叉树的删除 黑科技:利用完全二叉树特性快速定位
六、灵魂拷问室 💬
面试官:如何证明删除后的树仍然是BST?
答:中序遍历结果保持严格递增序即为合法BST
面试官:如何处理重复元素的删除?
答:方案一:仅删除第一个遇到的节点;方案二:设计支持重复值的BST结构
面试官:删除操作会影响哪些树属性?
答:高度、平衡因子、父指针、子树大小(若维护)
七、下期预告
《树的旋转操作——像体操运动员一样玩转平衡》 🚀 亮点剧透:
- 🤸 AVL树的四种旋转姿势
- ⚖️ 红黑树的颜色翻转魔术
- 🧮 平衡因子计算秘籍
graph RL
A((不平衡树)) --> B{旋转手术}
B -->|左旋| C((平衡树))
B -->|右旋| D((平衡树))
🌟 算法留言墙:你曾在删除操作中踩过哪些坑?在评论区分享你的惊险故事吧!
pie
title 删除操作错误类型统计
"指针未更新" : 38
"平衡性破坏" : 25
"内存泄漏" : 18
"递归溢出" : 12
"其他" : 7