【C++】20.二叉搜索树

文章目录

  • 1. 二叉搜索树的概念
  • 2. 二叉搜索树的性能分析
  • 3. 二叉搜索树的插入
  • 4. 二叉搜索树的查找
  • 5. 二叉搜索树的删除
  • 6. 二叉搜索树的实现代码
  • 7. 二叉搜索树key和key/value使用场景
    • 7.1 key搜索场景:
    • 7.2 key/value搜索场景:
    • 7.3 主要区别:
    • 7.4 key/value二叉搜索树代码实现


1. 二叉搜索树的概念

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

  • 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值

  • 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值

  • 它的左右子树也分别为二叉搜索树

  • 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值

cf94bfb8af2ae63cbe10b5bdf3e755fe


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

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:log2N

最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为:N

所以综合而言二叉搜索树增删查改时间复杂度为:O(N)那么这样的效率显然是无法满足我们需求的,我们后续课程需要继续讲解二叉搜索树的变形,平衡二叉搜索树AVL树和红黑树,才能适用于我们在内存中存储和搜索数据。

另外需要说明的是,二分查找也可以实现O(log2N) 级别的查找效率,但是二分查找有两大缺陷:

  1. 需要存储在支持下标随机访问的结构中,并且有序。
  2. 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。

这里也就体现出了平衡二叉搜索树的价值。

c7b3d34c04166308b671b99604f6a765


3. 二叉搜索树的插入

插入的具体过程如下:

  1. 树为空,则直接新增结点,赋值给root指针
  2. 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
  3. 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走)

9fe7486fce26447f482925ae976ab4ce

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

2dd3599c7a61c81540b5c035e4e6a76b

108ce1c72132107736dcdae2d5fbf962


4. 二叉搜索树的查找

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

ca3ae8a946b4f1715abd80a20b1d31f3


5. 二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回false

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N

  1. 要删除结点N左右孩子均为空
  2. 要删除的结点N左孩子为空,右孩子结点不为空
  3. 要删除的结点N右孩子为空,左孩子结点不为空
  4. 要删除的结点N左右孩子结点均不为空对应以上四种情况的解决方案:
  5. N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是一样的)
  6. N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点
  7. N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点
  8. 无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意一个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是NR的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。

简单来说就是:

1bb2cc10f2c8db46309a7a1684fb0f42.png

  1. 删除叶子节点–>直接删
  2. 被删除结点无左子树–>将该节点右子树的根代替该节点
    例如:要删除23,可以把34代替它。
    ab7dd5442f3cfbfbb18e2d5be831e42a.png
  3. 被删除结点无右子树–>将该节点左子树的根代替该节点
    例如:要删除8,可以把1代替它。
    c401cd956e0e469a66714a2d963776ef.png
  4. 左右都有孩子–>找它的前驱(左子树里最大的元素)或者后继(右子树里最小的元素)
    85114bd1a42dd8be073ecb22148deee1.png
    例如:删除39
    可以直接把34换过去,也可以把46换过去,46换过去就和上面第二点一样。

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;   // 类型重定义,简化代码
public:// 插入关键码keybool 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                           // key已存在{return false;}}// 创建新节点并连接到合适位置cur = new Node(key);if (parent->_key < key)parent->_right = cur;elseparent->_left = cur;return true;}// 查找关键码key是否存在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;                      // 没找到}// 删除关键码keybool 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;elseparent->_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;elseparent->_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;// 删除最小节点if (rightMinP->_left == rightMin)rightMinP->_left = rightMin->_right;elserightMinP->_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. 二叉搜索树key和key/value使用场景

7.1 key搜索场景:

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

场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。

场景2:检查一篇英文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。


在二叉搜索树中,key实际上是可以修改的,但是不建议直接修改key,原因如下:

  1. 破坏二叉搜索树的性质
例如这样的树:5/ \3   7/ \6   8如果我们直接修改节点6的key为95/ \3   7/ \9   8   // 破坏了BST性质!
  1. 正确的修改方式
// 如果需要修改key,应该:
1. 先删除旧的节点
2. 再插入新的节点// 伪代码:
void ModifyKey(const K& oldKey, const K& newKey)
{Node* node = Find(oldKey);if (node){V value = node->_value;  // 保存原来的valueErase(oldKey);          // 删除旧节点Insert(newKey, value);  // 插入新节点}
}

总结:

  • key可以修改,但不建议直接修改
  • 如果需要修改key,应该通过删除旧节点并插入新节点的方式
  • 这样可以保证二叉搜索树的基本性质不被破坏
  • 保持树结构的正确性和查找效率

7.2 key/value搜索场景:

每一个关键码key,都有与之对应的值valuevalue可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的valuekey/value的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树性质了,可以修改value

场景1:简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文。

场景2:商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场。

场景3:统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词,1),单词存在,则++单词对应的次数。


7.3 主要区别:

  1. 只有key的场景:
    • 只需要判断存在性
    • 不能修改key
    • 数据结构更简单
  2. key-value的场景:
    • 需要存储和查找关联数据
    • 可以修改value
    • 不能修改key
    • 适合需要映射关系的场景

注意事项:

  • 两种情况都不能修改key,因为会破坏二叉搜索树的性质
  • key-value结构更灵活,但占用更多内存
  • 选择哪种结构取决于具体应用需求

7.4 key/value二叉搜索树代码实现

// 二叉搜索树节点结构,支持key-value对
template<class K, class V>
struct BSTNode
{// pair<K, V> _kv;  // 也可以用pair存储键值对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){}
};// 二叉搜索树类
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;elseparent->_left = cur;return true;}// 查找指定key的节点,返回节点指针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;elsereturn cur;  // 找到返回节点指针}return nullptr;    // 没找到返回空}// 删除指定key的节点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;elseparent->_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;elseparent->_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;// 删除替换节点if (rightMinP->_left == rightMin)rightMinP->_left = rightMin->_right;elserightMinP->_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;elsecout << "无此单词,请重新输入" << 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);  // 第一次出现elseret->_value++;            // 已存在,计数加1}// 打印统计结果countTree.InOrder();return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/65366.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Opencv图片的旋转和图片的模板匹配

图片的旋转和图片的模板匹配 目录 图片的旋转和图片的模板匹配1 图片的旋转1.1 numpy旋转1.1.1 函数1.1.2 测试 1.2 opencv旋转1.2.1 函数1.2.2 测试 2 图片的模板匹配2.1 函数2.2 实际测试 1 图片的旋转 1.1 numpy旋转 1.1.1 函数 np.rot90(kl,k1)&#xff0c;k1逆时针旋转9…

重温设计模式--13、策略模式

策略模式介绍 文章目录 策略模式介绍C 代码示例 策略模式是一种行为设计模式&#xff0c;它允许在运行时选择算法的行为。该模式将算法的定义和使用分离开来&#xff0c;使得算法可以独立于使用它的客户端而变化&#xff0c;提高了代码的灵活性和可维护性。 其主要包含以下几个…

【GOOD】A Survey of Deep Graph Learning under Distribution Shifts

深度图学习在分布偏移下的综述&#xff1a;从图的分布外泛化到自适应 Northwestern University, USA Repository Abstract 图上的分布变化——训练和使用图机器学习模型之间的数据分布差异——在现实世界中普遍存在&#xff0c;并且通常不可避免。这些变化可能会严重恶化模…

【Axure高保真原型】环形进度条(开始暂停效果)

今天和大家分享环形进度条&#xff08;开始暂停效果&#xff09;的原型模版&#xff0c;效果包括&#xff1a; 点击开始按钮&#xff0c;可以环形进度条开始读取&#xff0c;中部百分比显示环形的读取进度&#xff1b; 在读取过程中&#xff0c;点击暂停按钮&#xff0c;可以随…

Euler 21.10(华为欧拉)安装oracle19c-RAC

1. Euler 21.10安装oracle19c-RAC 1.1. 环境规划 1.1.1. 主机规划 hostname IP 实例名 hfdb90 192.168.40.90 hfdb1 hfdb91 192.168.40.90 hfdb2 系统版本 BigCloud Enterprise Linux For Euler 21.10 (GNU/Linux 4.19.90-2107.6.0.0100.oe1.bclinux.x86_64 x86_6…

【python】matplotlib(radar chart)

文章目录 1、功能描述和原理介绍2、代码实现3、效果展示4、完整代码5、多个雷达图绘制在一张图上6、参考 1、功能描述和原理介绍 基于 matplotlib 实现雷达图的绘制 一、雷达图的基本概念 雷达图&#xff08;Radar Chart&#xff09;&#xff0c;也被称为蛛网图或星型图&…

Business Cooperation Process

Business Cooperation Process 商务合作基本流程 并不是每个人都能做到言而有信的&#xff0c;因此还是需要流程来约束的。

模式识别-Ch2-分类错误率

分类错误率 最小错误率贝叶斯决策 样本 x x x的错误率&#xff1a; 任一决策都可能会有错误。 P ( error ∣ x ) { P ( w 2 ∣ x ) , if we decide x as w 1 P ( w 1 ∣ x ) , if we decide x as w 2 P(\text{error}|\mathbf{x})\begin{cases} P(w_2|\mathbf{x}), &…

Milvus×全诊通:从导诊到智能超声,如何将人效比翻倍

AI与智慧医疗的结合已是未来发展的必然趋势。近年来&#xff0c;国家卫健委推崇智慧医疗和AI技术&#xff0c;如智能导诊、预问诊、辅助诊断等&#xff0c;以提高医疗服务效率和诊断准确性&#xff0c;改善患者就医体验。 全诊通是一家专注于医疗SaaS和人工智能的公司&#xff…

自动驾驶相关知识学习笔记

一、概要 因为想知道SIL、HIL是什么仿真工具&#xff0c;故而浏览了自动驾驶相关的知识。 资料来源《自动驾驶——人工智能理论与实践》胡波 林青 陈强 著&#xff1b;出版时间&#xff1a;2023年3月 二、图像的分类、分割与检测任务区别 如图所示&#xff0c;这些更高阶的…

springcloud 介绍

Spring Cloud是一个基于Spring Boot的微服务架构解决方案集合&#xff0c;它提供了一套完整的工具集&#xff0c;用于快速构建分布式系统。在Spring Cloud的架构中&#xff0c;服务被拆分为一系列小型、自治的微服务&#xff0c;每个服务运行在其独立的进程中&#xff0c;并通过…

【PLL】非线性瞬态性能

频率捕获、跟踪响应&#xff0c;是大信号非线性行为锁相范围内的相位、频率跟踪&#xff0c;不是非线性行为 所以&#xff1a;跟踪&#xff0c;是线性区域&#xff1b;捕获&#xff0c;是大信号、非线性区域 锁定范围&#xff1a;没有周跳&#xff08;cycle-slipping&#xff0…

QML学习(七) 学习QML时,用好Qt设计器,快速了解各个组件的属性

在初步学习QML时&#xff0c;特别建议看看Qt设计器&#xff0c;先利用Qt Quick设计师的使用&#xff0c;快速的对Qt Quick的各个组件及其常用的属性&#xff0c;有个初步的了解和认识。如果初始学习一上来直接以代码形式开干&#xff0c;很容易一头雾水。而设计器以最直白的所见…

find 查找文件grep匹配数据

一、find介绍 1. find . -iname "*.txt"查找当前目录下各个文件夹下的txt属性的文件(i忽略大小写)。 2.find . -type f 查找当前目录下各个文件夹下的文件 3.find . -type d 查找当前目录下各个文件夹下的目录 4.find . -type f | xargs grep -ain -E "匹配…

Mac上鸿蒙配置HDC报错:zsh: command not found: hdc -v

这个问题困扰了好久&#xff0c;按照官方文档去配置的&#xff0c;就是会一直报错&#xff0c;没有配置成功&#xff0c;主要原因是官网ide的路径可能和你本地的ide的路径不一致&#xff0c;因为官网的ide版本可能是最新的 一.先查找你本地的toolchains目录在哪里&#xff0c;…

CS·GO搬砖流程详细版

说简单点&#xff0c;就是Steam买了然后BUFF上卖&#xff0c;或许大家都知道这点&#xff0c;但就是一些操作和细节问题没那么明白。我相信&#xff0c;你看完这篇文章以后&#xff0c;至少会有新的认知。 好吧&#xff0c;废话少说&#xff0c;直接上实操&#xff01; 首先准…

“深入浅出”系列之FFmpeg:(1)音视频开发基础

我的音视频开发大部分内容是跟着雷霄骅大佬学习的&#xff0c;所以笔记也是跟雷老师的博客写的。 一、音视频相关的基础知识 首先播放一个视频文件的流程如下所示&#xff1a; FFmpeg的作用就是将H.264格式的数据转换成YUV格式的数据&#xff0c;然后SDL将YUV显示到电脑屏幕上…

【Linux】Linux开发:GDB调试器与Git版本控制工具指南

Linux相关知识点可以通过点击以下链接进行学习一起加油&#xff01;初识指令指令进阶权限管理yum包管理与vim编辑器GCC/G编译器make与Makefile自动化构建 在 Linux 开发中&#xff0c;GDB 调试器和 Git 版本控制工具是开发者必备的利器。GDB 帮助快速定位代码问题&#xff0c;G…

【设计模式-2】23 种设计模式的分类和功能

在软件工程领域&#xff0c;设计模式是解决常见设计问题的经典方案。1994 年&#xff0c;Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides&#xff08;四人帮&#xff0c;GoF&#xff09;在《设计模式&#xff1a;可复用面向对象软件的基础》一书中系统性地总结了…

谷粒商城-高级篇完结-Sleuth+Zipkin 服务链路追踪

1、基本概念和整合 1.1、为什么用 微服务架构是一个分布式架构&#xff0c;它按业务划分服务单元&#xff0c;一个分布式系统往往有很多个服务单元。由于服务单元数量众多&#xff0c;业务的复杂性&#xff0c;如果出现了错误和异常&#xff0c;很难去定位 。主要体现在&#…