二叉搜索树(下篇):删除、优化与应用

🗑️ 二叉搜索树的删除操作

删除的四种情况

删除操作是二叉搜索树中最复杂的部分,需要处理四种不同的情况。假设要删除的节点为N

  1. 情况1:N是叶子节点(左右孩子均为空)
  2. 情况2:N只有右孩子(左孩子为空)
  3. 情况3:N只有左孩子(右孩子为空)
  4. 情况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

替换法步骤

  1. 找到N右子树的最小节点R(或左子树的最大节点)
  2. R的值复制到N
  3. 删除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中的mapset底层就是红黑树(一种平衡二叉搜索树):

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;
}

二叉搜索树的变体

  1. 伸展树(Splay Tree):最近访问的节点移动到根部,提高局部性
  2. Treap:结合二叉搜索树和堆的特性
  3. B树:多路搜索树,用于磁盘存储
  4. 线段树:用于区间查询的特殊二叉搜索树

📝 最佳实践与注意事项

代码实现要点

  1. 内存管理:注意在析构函数中释放所有节点
  2. 异常安全:确保操作异常时资源不泄漏
  3. 模板设计:支持多种数据类型
  4. 迭代器支持:实现前向、双向迭代器

常见错误

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;
}

🎓 总结与展望

二叉搜索树的核心价值

  1. 教学价值:理解树结构的基础,掌握递归和分治思想
  2. 实用价值:平衡后的二叉搜索树是许多高级数据结构的基础
  3. 算法价值:许多算法问题可以转化为树上的操作

学习路径建议

  1. 初级阶段:掌握基本操作(插入、查找、删除)
  2. 中级阶段:理解平衡机制(AVL树、红黑树)
  3. *高级阶段:学习变体和扩展(B树、线段树、Treap等)
  4. *实践阶段:在项目中应用,理解STL容器的实现原理

*未来发展方向

  • 并发二叉搜索树:支持多线程并发访问
  • 持久化二叉搜索树:支持版本历史查询
  • 外部存储优化:针对SSD、磁盘的优化版本
  • 机器学习应用:决策树等机器学习算法的底层结构

理解二叉搜索树是掌握高级数据结构的钥匙

在下期博客我将带来STL中map和set的介绍和使用~~~

相关推荐
极简车辆控制2 小时前
基于LQR全主动七自由度全车悬架车身姿态控制
算法
superman超哥2 小时前
仓颉借用检查器工作原理深度解析
c语言·开发语言·c++·python·仓颉
s09071362 小时前
常用FPGA实现的图像处理算法
图像处理·算法·fpga开发
鱼鱼块2 小时前
二叉搜索树:让数据在有序中生长的智慧之树
javascript·数据结构·面试
core5122 小时前
SVM (支持向量机):寻找最完美的“分界线”
算法·机器学习·支持向量机·svm
TG:@yunlaoda360 云老大2 小时前
华为云国际站代理商的DDM支持哪些拆分算法?
数据库·算法·华为云
qq_430855883 小时前
线代第二章矩阵第五、六、七节矩阵的转置、方阵的行列式、方阵的伴随矩阵
线性代数·算法·矩阵
CoderCodingNo3 小时前
【GESP】C++五级真题(数论考点) luogu-B3871 [GESP202309 五级] 因数分解
开发语言·c++
jianfeng_zhu3 小时前
二叉树的中序线索化,并通过线索化后遍历二叉树
数据结构·链表