🗑️ 二叉搜索树的删除操作
删除的四种情况
删除操作是二叉搜索树中最复杂的部分,需要处理四种不同的情况。假设要删除的节点为N:
- 情况1:N是叶子节点(左右孩子均为空)
- 情况2:N只有右孩子(左孩子为空)
- 情况3:N只有左孩子(右孩子为空)
- 情况4:N有两个孩子(左右孩子均不为空)
让我们通过示例树来理解这四种情况:
8
3
10
1
6
4
7
14
13
null
null
情况1:删除叶子节点
删除节点1(叶子节点)
- 直接将父节点的对应指针置空
- 释放节点内存
cpp
// 情况1处理代码片段
if (parent->_left == cur) {
parent->_left = nullptr; // 父节点的左指针置空
} else {
parent->_right = nullptr; // 父节点的右指针置空
}
delete cur;
情况2和3:删除只有一个孩子的节点
删除节点10(只有右孩子14)
- 将节点10的右孩子14连接到节点10的父节点8
删除节点14(只有左孩子13)
- 将节点14的左孩子13连接到节点14的父节点10
删除后
8
3
14
删除前
8
3
10
null
14
cpp
// 情况2:只有右孩子
if (cur->_left == nullptr) {
if (parent == nullptr) {
_root = cur->_right; // 删除的是根节点
} else if (parent->_left == cur) {
parent->_left = cur->_right; // cur是父节点的左孩子
} else {
parent->_right = cur->_right; // cur是父节点的右孩子
}
delete cur;
}
// 情况3:只有左孩子(对称处理)
else if (cur->_right == nullptr) {
if (parent == nullptr) {
_root = cur->_left;
} else if (parent->_left == cur) {
parent->_left = cur->_left;
} else {
parent->_right = cur->_left;
}
delete cur;
}
情况4:删除有两个孩子的节点(替换法)
这是最复杂的情况,不能直接删除 ,因为两个子节点需要重新安置。解决方案是替换法删除:
删除节点3后
8
4
10
1
6
null
7
null
5
删除节点3前
8
3
10
1
6
4
7
null
5
替换法步骤:
- 找到
N的右子树的最小节点R(或左子树的最大节点) - 将
R的值复制到N - 删除
R节点(此时R最多只有一个孩子,回归到情况2或3)
cpp
// 情况4:有两个孩子,使用替换法
else {
// 找到右子树的最小节点
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left) {
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
// 用右子树最小节点的值替换当前节点值
cur->_key = rightMin->_key;
// 删除右子树的最小节点
if (rightMinParent->_left == rightMin) {
rightMinParent->_left = rightMin->_right;
} else {
// 处理右子树的根就是最小节点的情况
rightMinParent->_right = rightMin->_right;
}
delete rightMin;
}
边界情况处理
特殊情况:删除根节点
cpp
// 在情况2和3中处理根节点删除
if (parent == nullptr) {
_root = (cur->_left != nullptr) ? cur->_left : cur->_right;
delete cur;
return true;
}
特殊情况:右子树的最小节点就是右子树的根
删除后
8
6
10
1
7
删除前
8
3
10
1
6
null
7
cpp
// 处理右子树根就是最小节点的情况
if (rightMinParent->_left == rightMin) {
rightMinParent->_left = rightMin->_right;
} else {
// 这种情况发生在rightMinParent就是cur时
rightMinParent->_right = rightMin->_right;
}
📈 性能分析与优化策略
二叉搜索树的性能问题
输入数据
数据特征
完全随机
有序/接近有序
平衡的二叉搜索树
高度≤log₂N
退化的二叉搜索树
高度≈N
查找:O(logN)
插入:O(logN)
删除:O(logN)
查找:O(N)
插入:O(N)
删除:O(N)
优化策略:平衡二叉搜索树
为了解决二叉搜索树退化的问题,人们发明了多种平衡二叉搜索树:
| 平衡树类型 | 平衡标准 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|---|
| AVL树 | 严格平衡 左右子树高度差≤1 | 查询效率高 适合查找密集型 | 维护成本高 插入删除频繁时效率低 | 数据库索引 内存受限系统 |
| 红黑树 | 近似平衡 通过颜色约束 | 综合性能好 插入删除效率高 | 实现复杂 查询略慢于AVL树 | C++ STL Java HashMap Linux内核 |
| B树/B+树 | 多路平衡树 | 适合磁盘I/O 减少磁盘访问 | 内存中操作复杂 | 文件系统 数据库系统 |
从二叉搜索树到红黑树
二叉搜索树
发现问题:可能退化
解决方案:保持平衡
选择平衡策略
AVL树:严格平衡
红黑树:近似平衡
其他平衡树
适合查询多
更新少的场景
综合性能好
实际应用广泛
🔑 二叉搜索树的应用场景
场景1:纯key模式(键的存在性检查)
适用场景:只需要判断key是否存在,不需要关联value。
实例1:小区车牌识别系统
cpp
// 建立车牌白名单
BSTree<string> carWhiteList;
carWhiteList.Insert("京A12345");
carWhiteList.Insert("京B67890");
// ...
// 车辆进入时检查
string plateNumber = "京A12345";
if (carWhiteList.Find(plateNumber)) {
cout << "欢迎进入,自动抬杆" << endl;
} else {
cout << "非本小区车辆,禁止进入" << endl;
}
实例2:单词拼写检查器
cpp
// 加载词典
BSTree<string> dictionary;
LoadDictionary(dictionary); // 从文件加载所有单词
// 检查单词拼写
string word = "accomodation"; // 错误拼写
if (!dictionary.Find(word)) {
cout << "拼写错误:" << word << endl;
// 建议更正...
}
场景2:key/value模式(键值对存储)
适用场景:每个key关联一个value,支持通过key快速查找value。
实例1:中英字典
cpp
BSTree<string, string> dict;
dict.Insert("apple", "苹果");
dict.Insert("banana", "香蕉");
dict.Insert("computer", "计算机");
// 查询
string englishWord = "apple";
auto result = dict.Find(englishWord);
if (result) {
cout << englishWord << " 的中文是: " << result->value << endl;
}
实例2:停车场计费系统
cpp
class ParkingSystem {
BSTree<string, time_t> parkingRecords; // 车牌->入场时间
public:
void CarEnter(string plateNumber) {
time_t enterTime = time(nullptr);
parkingRecords.Insert(plateNumber, enterTime);
cout << plateNumber << " 入场时间: " << ctime(&enterTime);
}
double CarExit(string plateNumber) {
auto record = parkingRecords.Find(plateNumber);
if (!record) {
cout << "未找到入场记录" << endl;
return 0;
}
time_t exitTime = time(nullptr);
double duration = difftime(exitTime, record->value) / 3600.0; // 小时
double fee = CalculateFee(duration);
parkingRecords.Erase(plateNumber);
return fee;
}
};
实例3:单词频率统计
cpp
// 统计文章中单词出现次数
BSTree<string, int> wordFrequency;
vector<string> article = {"the", "quick", "brown", "fox", "jumps",
"over", "the", "lazy", "dog"};
for (const string& word : article) {
auto node = wordFrequency.Find(word);
if (node == nullptr) {
wordFrequency.Insert(word, 1); // 第一次出现
} else {
node->value++; // 增加计数
}
}
// 输出结果
wordFrequency.InOrder(); // 按字母顺序输出单词及其频率
💻 完整实现:key/value二叉搜索树
数据结构定义
cpp
template<class K, class V>
struct BSTNode {
K key;
V value;
BSTNode<K, V>* left;
BSTNode<K, V>* right;
BSTNode(const K& k, const V& v)
: key(k), value(v), left(nullptr), right(nullptr) {}
};
完整的BSTree类实现
cpp
template<class K, class V>
class BSTree {
typedef BSTNode<K, V> Node;
private:
Node* _root = nullptr;
// 内部辅助函数
void _InOrder(Node* root);
void Destroy(Node* root);
Node* Copy(Node* root);
public:
// 构造、析构、拷贝
BSTree() = default;
BSTree(const BSTree<K, V>& t);
BSTree<K, V>& operator=(BSTree<K, V> t);
~BSTree();
// 基本操作
bool Insert(const K& key, const V& value);
Node* Find(const K& key);
bool Erase(const K& key);
// 遍历
void InOrder();
};
使用示例:单词频率统计器
cpp
int main() {
// 示例1:统计水果出现次数
string fruits[] = {"苹果", "西瓜", "苹果", "西瓜", "苹果",
"苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉"};
BSTree<string, int> fruitCounter;
for (const auto& fruit : fruits) {
auto node = fruitCounter.Find(fruit);
if (node == nullptr) {
fruitCounter.Insert(fruit, 1); // 第一次出现
} else {
node->value++; // 增加计数
}
}
cout << "水果出现次数统计:" << endl;
fruitCounter.InOrder();
// 示例2:简单英汉词典
BSTree<string, string> dict;
dict.Insert("algorithm", "算法");
dict.Insert("data structure", "数据结构");
dict.Insert("binary tree", "二叉树");
string word;
while (cout << "请输入英文单词: ", cin >> word) {
if (word == "exit") break;
auto result = dict.Find(word);
if (result) {
cout << "中文释义: " << result->value << endl;
} else {
cout << "未找到该单词" << endl;
cout << "是否添加新词条? (y/n): ";
char choice;
cin >> choice;
if (choice == 'y' || choice == 'Y') {
string meaning;
cout << "请输入中文释义: ";
cin >> meaning;
dict.Insert(word, meaning);
}
}
}
return 0;
}
🚀 进阶:从二叉搜索树到实际应用
STL中的二叉搜索树
C++ STL中的map和set底层就是红黑树(一种平衡二叉搜索树):
cpp
// STL的map用法(底层是红黑树)
#include <map>
#include <string>
std::map<std::string, int> wordCount;
wordCount["hello"] = 1;
wordCount["world"] = 2;
// 查找
auto it = wordCount.find("hello");
if (it != wordCount.end()) {
cout << it->first << ": " << it->second << endl;
}
// STL的set用法(底层也是红黑树)
#include <set>
std::set<int> uniqueNumbers;
uniqueNumbers.insert(1);
uniqueNumbers.insert(2);
uniqueNumbers.insert(1); // 不会重复插入
if (uniqueNumbers.find(1) != uniqueNumbers.end()) {
cout << "1存在于集合中" << endl;
}
二叉搜索树的变体
- 伸展树(Splay Tree):最近访问的节点移动到根部,提高局部性
- Treap:结合二叉搜索树和堆的特性
- B树:多路搜索树,用于磁盘存储
- 线段树:用于区间查询的特殊二叉搜索树
📝 最佳实践与注意事项
代码实现要点
- 内存管理:注意在析构函数中释放所有节点
- 异常安全:确保操作异常时资源不泄漏
- 模板设计:支持多种数据类型
- 迭代器支持:实现前向、双向迭代器
常见错误
cpp
// 错误示例:忘记处理父节点为空的情况
bool Erase(const K& key) {
// ... 找到要删除的节点cur
if (cur->_left == nullptr) {
// 错误:没有检查parent是否为空
if (parent->_left == cur) { // 如果cur是根节点,parent为空!
parent->_left = cur->_right;
} else {
parent->_right = cur->_right;
}
delete cur;
}
// ...
}
// 正确做法:始终检查parent是否为空
if (parent == nullptr) {
_root = cur->_right; // 删除的是根节点
} else if (parent->_left == cur) {
parent->_left = cur->_right;
} else {
parent->_right = cur->_right;
}
🎓 总结与展望
二叉搜索树的核心价值
- 教学价值:理解树结构的基础,掌握递归和分治思想
- 实用价值:平衡后的二叉搜索树是许多高级数据结构的基础
- 算法价值:许多算法问题可以转化为树上的操作
学习路径建议
- 初级阶段:掌握基本操作(插入、查找、删除)
- 中级阶段:理解平衡机制(AVL树、红黑树)
- *高级阶段:学习变体和扩展(B树、线段树、Treap等)
- *实践阶段:在项目中应用,理解STL容器的实现原理
*未来发展方向
- 并发二叉搜索树:支持多线程并发访问
- 持久化二叉搜索树:支持版本历史查询
- 外部存储优化:针对SSD、磁盘的优化版本
- 机器学习应用:决策树等机器学习算法的底层结构
理解二叉搜索树是掌握高级数据结构的钥匙。
在下期博客我将带来STL中map和set的介绍和使用~~~
