当前位置: 首页 > news >正文

C++数据结构 : 二叉搜索树

C++数据结构 : 二叉搜索树

目录

  • C++数据结构 : 二叉搜索树
    • 引言
    • 1. 二叉搜索树的概念
    • 2. 二叉搜索树的性能分析
    • 3. 二叉搜索树的插入
    • 4. 二叉搜索树的查找
    • 5. 二叉搜索树的删除
    • 6. 二叉搜索树的实现代码
    • 7. 二叉搜索树`key`和`key/value`使用场景

引言

二叉搜索树(BST)是一种高效的数据结构,通过有序的树形存储实现快速查找、插入和删除操作。其核心特性是:左子节点的值 ≤ 当前节点值 ≤ 右子节点的值。

本文将详解BST的原理、实现及应用,包括基础操作、性能分析,以及keykey/value两种模式的代码实现,帮助你在实际开发中灵活运用这一结构。


1. 二叉搜索树的概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值;
  • 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值;
  • 它的左右子树也分别为二叉搜索树。
  • 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值。

在这里插入图片描述


2. 二叉搜索树的性能分析

二叉搜索树的时间复杂度分析

  • 最优情况下,二叉搜索树为完全二叉树(或接近完全二叉树),其高度为 log₂N;
  • 最差情况下,二叉搜索树退化为单支树(或类似单支),其高度为 N。
  • 综合而言,二叉搜索树增删查改的时间复杂度为 O(N)。这样的效率显然无法满足需求,因此后续需要学习二叉搜索树的变形——平衡二叉搜索树(AVL 树和红黑树),才能适用于内存中的数据存储和搜索。

二分查找的局限性

虽然二分查找可以实现 O(log₂N) 的查找效率,但它有两大缺陷:(1) 数据必须存储在支持下标随机访问的有序结构中;(2) 插入和删除效率很低,因为需要移动数据以维护顺序。这也体现了平衡二叉搜索树的优势。

在这里插入图片描述


3. 二叉搜索树的插入

二叉搜索树插入的具体过程如下

  • 若树为空,则直接新增结点,赋值给root指针;
  • 若树不空,则按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,直至找到空位置并插入新结点;
  • 若支持插入相等的值,则插入值与当前结点相等的值可统一往右或往左走(需保持逻辑一致性,避免左右随机选择),最终找到空位置并插入新结点。

在这里插入图片描述

在这里插入图片描述


4. 二叉搜索树的查找

  1. 从根开始比较,查找x,x比根的值大则往右边走查找,x比根值小则往左边走查找;
  2. 最多查找高度次,走到空还没找到,则该值不存在;
  3. 如果不支持插入相等的值,找到x即可返回;
  4. 如果支持插入相等的值,意味着有多个x存在,一般要求查找中序的第一个x。如下图,查找3,要找到1的右孩子的那个3返回。

在这里插入图片描述


5. 二叉搜索树的删除

二叉搜索树删除操作流程

首先查找元素是否在二叉搜索树中,如果不存在,则返回false;如果存在则分以下四种情况处理(假设要删除的结点为N):

  1. 要删除的结点N左右孩子均为空;
  2. 要删除的结点N左孩子为空,右孩子结点不为空;
  3. 要删除的结点N右孩子为空,左孩子结点不为空;
  4. 要删除的结点N左右孩子结点均不为空。

对应解决方案:

  1. 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或3处理,效果相同);
  2. 把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点;
  3. 把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点;
  4. 无法直接删除N结点(需替换法):找N左子树的值最大结点R(最右结点)或右子树的值最小结点R(最左结点)替代N(交换N和R的值),转而删除R结点(此时R必符合情况2或3,可直接删除)。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


6. 二叉搜索树的实现代码

// 二叉搜索树节点模板类
template<class K>
struct BSTNode {K _key;                // 节点存储的关键值BSTNode<K>* _left;     // 左子节点指针BSTNode<K>* _right;    // 右子节点指针// 构造函数,初始化节点BSTNode(const K& key): _key(key)        // 初始化关键值, _left(nullptr)   // 左子树初始为空, _right(nullptr)  // 右子树初始为空{}
};// 二叉搜索树模板类
template<class K>
class BSTree {typedef BSTNode<K> Node;  // 类型别名,方便使用//using Node = BSTNode<K> 也可以使用using平替typedef进行类型重定义
public:// 插入元素bool Insert(const K& key) {// 如果树为空,直接创建根节点if (_root == nullptr) {_root = new Node(key);return true;}Node* parent = nullptr;  // 记录父节点Node* cur = _root;       // 当前节点指针// 查找插入位置while (cur) {if (cur->_key < key) {       // 如果当前节点值小于key,向右子树查找parent = cur;cur = cur->_right;}else if (cur->_key > key) {  // 如果当前节点值大于key,向左子树查找parent = cur;cur = cur->_left;}else {  // 如果值已存在,插入失败return false;}}// 创建新节点cur = new Node(key);// 将新节点连接到父节点if (parent->_key < key) {parent->_right = cur;  // 作为右孩子}else {parent->_left = cur;    // 作为左孩子}return true;}// 查找元素bool Find(const K& key) {Node* cur = _root;  // 从根节点开始查找while (cur) {if (cur->_key < key) {       // 当前值小于key,向右子树查找cur = cur->_right;}else if (cur->_key > key) {  // 当前值大于key,向左子树查找cur = cur->_left;}else {  // 找到目标值return true;}}return false;  // 未找到}// 删除元素bool Erase(const K& key) {Node* parent = nullptr;  // 父节点指针Node* cur = _root;       // 当前节点指针while (cur) {if (cur->_key < key) {       // 当前值小于key,向右子树查找parent = cur;cur = cur->_right;}else if (cur->_key > key) {  // 当前值大于key,向左子树查找parent = cur;cur = cur->_left;}else {  // 找到要删除的节点// 情况1和2:节点有0或1个子节点if (cur->_left == nullptr) {  // 左子树为空if (parent == nullptr) {   // 删除的是根节点_root = cur->_right;}else {// 更新父节点的指针if (parent->_left == cur) {parent->_left = cur->_right;}else {parent->_right = cur->_right;}}delete cur;return true;}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;return true;}else {  // 情况3:节点有2个子节点(替换法删除)// 找到右子树的最小节点(中序后继)Node* rightMinP = cur;       // 最小节点的父节点Node* rightMin = cur->_right; // 从右子树开始查找while (rightMin->_left) {    // 一直向左找最小值rightMinP = rightMin;rightMin = rightMin->_left;}// 用最小节点的值替换当前节点的值cur->_key = rightMin->_key;// 删除最小节点if (rightMinP->_left == rightMin) {rightMinP->_left = rightMin->_right;}else {rightMinP->_right = rightMin->_right;}delete rightMin;return true;}}}return false;  // 未找到要删除的节点}// 中序遍历(外部接口)void InOrder() {_InOrder(_root);  // 调用内部递归函数cout << endl;}private:// 内部递归中序遍历函数void _InOrder(Node* root) {if (root == nullptr) {  // 递归终止条件return;}_InOrder(root->_left);         // 遍历左子树cout << root->_key << " ";      // 访问当前节点_InOrder(root->_right);         // 遍历右子树}private:Node* _root = nullptr;  // 根节点指针
};

7. 二叉搜索树keykey/value使用场景

  • key搜索场景

    只有key作为关键码:结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉搜索树支持增删查,但不支持修改,因为修改key会破坏搜索树结构。

    应用场景示例

    1. 小区无人值守车库:小区车库仅允许购买了车位的业主车辆进入,物业会将已购车位业主的车牌号录入后台系统。车辆进入时扫描车牌,若在系统中则抬杆放行,否则提示“非本小区车辆,无法进入”。
    2. 英文拼写检查:将词库中所有单词存入二叉搜索树,读取文章中的单词并查找是否在树中。若不存在,则用波浪线标红提示拼写错误。
  • key/value搜索场景

    在二叉搜索树的键值对(key/value)存储结构中,每个关键码(key)都对应一个任意类型的值(value)。树的结点除了存储key外还需存储对应的value,增删查操作均以key为关键字按照二叉搜索树的规则进行比较,从而快速定位key对应的value。这种结构支持修改value但不允许修改key,因为修改key会破坏搜索树的性质。

    应用场景示例

    1. 中英字典:结点存储key(英文)和value(中文),输入英文即可检索对应中文;
    2. 无人车库管理:入场时记录车牌(key)和入场时间(value),出场时通过车牌查询入场时间,计算停车时长和费用;
    3. 单词统计:读取单词时,若未存在则插入(单词,1),若已存在则递增其计数(value)。
  • key/value二叉搜索树代码实现

    // 二叉搜索树(BST)节点模板类
    // K 表示键(key)类型,V 表示值(value)类型
    template<class K, class V>
    struct BSTNode {K _key;                 // 节点存储的键V _value;               // 节点存储的值BSTNode<K, V>* _left;   // 左子节点指针BSTNode<K, V>* _right;  // 右子节点指针// 构造函数BSTNode(const K& key, const V& value): _key(key), _value(value), _left(nullptr), _right(nullptr){}
    };// 二叉搜索树(BST)模板类
    template<class K, class V>
    class BSTree {typedef BSTNode<K, V> Node;  // 节点类型别名定义
    public:// 默认构造函数BSTree() = default;// 拷贝构造函数(深拷贝)BSTree(const BSTree<K, V>& t) {_root = Copy(t._root);}// 赋值运算符重载(使用拷贝交换技术)BSTree<K, V>& operator=(BSTree<K, V> t) {swap(_root, t._root);return *this;}// 析构函数~BSTree() {Destroy(_root);_root = nullptr;}// 插入键值对bool Insert(const K& key, const V& value) {if (_root == nullptr) {_root = new Node(key, value);return true;}Node* parent = nullptr;Node* cur = _root;// 查找合适的插入位置while (cur) {if (cur->_key < key) {parent = cur;cur = cur->_right;}else if (cur->_key > key) {parent = cur;cur = cur->_left;}else {return false;  // 键已存在,插入失败}}// 创建新节点cur = new Node(key, value);// 将新节点连接到父节点if (parent->_key < key) {parent->_right = cur;}else {parent->_left = cur;}return true;}// 查找键对应的节点Node* Find(const K& key) {Node* cur = _root;while (cur) {if (cur->_key < key) {cur = cur->_right;}else if (cur->_key > key) {cur = cur->_left;}else {return cur;  // 找到键对应的节点}}return nullptr;  // 未找到}// 删除键对应的节点bool Erase(const K& key) {Node* parent = nullptr;Node* cur = _root;// 查找要删除的节点while (cur) {if (cur->_key < key) {parent = cur;cur = cur->_right;}else if (cur->_key > key) {parent = cur;cur = cur->_left;}else {// 找到要删除的节点,分三种情况处理// 情况1:要删除的节点只有右子树或没有子树if (cur->_left == nullptr) {if (parent == nullptr) {  // 删除的是根节点_root = cur->_right;}else {if (parent->_left == cur) {parent->_left = cur->_right;}else {parent->_right = cur->_right;}}delete cur;return true;}// 情况2:要删除的节点只有左子树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;return true;}// 情况3:要删除的节点有左右子树else {// 找到右子树中最小的节点(替代节点)Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left) {rightMinP = rightMin;rightMin = rightMin->_left;}// 用替代节点的值替换当前节点的值cur->_key = rightMin->_key;cur->_value = rightMin->_value;// 删除替代节点if (rightMinP->_left == rightMin) {rightMinP->_left = rightMin->_right;}else {rightMinP->_right = rightMin->_right;}delete rightMin;return true;}}}return false;  // 未找到要删除的键}// 中序遍历(按键升序输出)void InOrder() {_InOrder(_root);cout << endl;}private:// 递归中序遍历辅助函数void _InOrder(Node* root) {if (root == nullptr) {return;}_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}// 递归销毁树辅助函数void Destroy(Node* root) {if (root == nullptr) return;Destroy(root->_left);Destroy(root->_right);delete root;}// 递归拷贝树辅助函数Node* Copy(Node* root) {if (root == nullptr) return nullptr;Node* newRoot = new Node(root->_key, root->_value);newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}private:Node* _root = nullptr;  // 树的根节点
    };// 主函数1:字典应用示例
    int main() {BSTree<string, string> dict;dict.Insert("left", "左边");dict.Insert("right", "右边");dict.Insert("insert", "插入");dict.Insert("string", "字符串");string str;while (cin >> str) {auto ret = dict.Find(str);if (ret) {cout << "->" << ret->_value << endl;}else {cout << "无此单词,请重新输入" << endl;}}return 0;
    }// 主函数2:统计水果出现次数示例
    int main() {string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };BSTree<string, int> countTree;for (const auto& str : arr) {auto ret = countTree.Find(str);if (ret == NULL) {countTree.Insert(str, 1);  // 第一次出现的水果}else {ret->_value++;  // 已存在的水果,计数增加}}countTree.InOrder();  // 输出统计结果return 0;
    }
    

相关文章:

  • 【Elasticsearch】使用脚本删除索引中的某个字段
  • SLOT:测试时样本专属语言模型优化,让大模型推理更精准!
  • 中车靶场,网络安全暑期实训营
  • FOFA网络空间测绘初学者指南:像探险家一样探索数字世界
  • 什么是数据驱动?以及我们应如何理解数据驱动?
  • ICMP与TCP端口:网络层与传输层解析
  • Flutter 实现6个验收码输入框
  • 实现单例模式的6种方法(Python)
  • 防爆手机VS普通手机,区别在哪里?
  • 获取oracle的HQL日志,采取参数日志,拼装SQL语句
  • Oracle初识
  • Java大师成长计划之第32天:使用Kubernetes进行Java应用编排与管理
  • C++学习-入门到精通【9】面向对象编程:继承
  • 低空经济数据湖架构设计方案
  • 贝壳后端golang面经
  • 浅浅学:XCP协议原理及应用
  • ctf.show pwn入门 堆利用-前置基础 pwn142
  • STM32 Keil工程搭建 (手动搭建)流程 2025年5月27日07:42:09
  • Excel常用公式全解析(1):从基础计算到高级应用
  • STM32之FreeRTOS移植(重点)
  • 西城区网站建设/淘宝产品关键词排名查询
  • wordpress授权插件/seo对各类网站的作用
  • 西宁软件网站建设/软文的概念
  • 南京市住房和城乡建设部网站/网站百度收录突然消失了
  • 建筑公司资质/铜川网站seo
  • 国外优秀app设计网站有哪些/北京seo外包