【数据结构进阶】二叉搜索树

在这里插入图片描述

🔥个人主页: Forcible Bug Maker
🔥专栏: C++ || 数据结构

目录

  • 🌈前言
  • 🔥二叉搜索树
  • 🔥 二叉搜索树的实现
    • ==Insert(插入)==
    • ==find(查找)==
    • ==erase(删除)==
    • ==destroy(析构)==
    • ==InOrder(中序遍历)==
    • ==拷贝构造==
  • 🔥二叉搜索树的应用
  • 🔥二叉搜索树的性能
  • 🌈结语

🌈前言

本篇博客主要内容:二叉搜索树的介绍及自实现

基础的二叉树在前面的C数据结构阶段已经讲过(初阶数据结构之—二叉树链式结构)。之前因为用C语言的话,实现更高级数据结构比较困难,所以并没有往后展开。到了现在,已经有了一定的C++功底,就可以开启我们数据结构进阶部分的内容了。对于二叉搜索树的特性了解,有助于后续更好的理解map和set的特性。本节课作为学习更高阶数据结构的基础,对后续学习来说至关重要。

🔥二叉搜索树

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

  • 若它的左子树不为空,则左子树上的所有结点都小于根节点的值
  • 若它的右子树不为空,则右子树上的所有结点都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

二叉搜索树的中序遍历是有序的。

在这里插入图片描述

🔥 二叉搜索树的实现

在这里插入图片描述
以下是需要实现的二叉搜索树的头文件内容

#pragma once
#include<iostream>namespace ForcibleBugMaker
{template<class K>struct BSTreeNode{BSTreeNode<K>(const K& k = K()):_key(k), _left(nullptr), _right(nullptr){}K _key;BSTreeNode<K>* _left;BSTreeNode<K>* _right;};template<class K>class BSTree{typedef BSTreeNode<K> Node;public:BSTree() = default;BSTree(const BSTree<K>& t);bool Insert(const K& key);Node* Find(const K& key);bool Erase(const K& key);~BSTree();void InOrder();private:Node* _root = nullptr;};
}

二叉搜索树的结点中有三个成员变量,分别是
_key:存储数据;_left:指向左子树;_right:指向右子树。将其在BSTree中typedef成Node方便后续使用。

Insert(插入)

二叉树的插入,主要考虑两种情况:

  1. 树为空,则直接新增结点,赋给root指针。
  2. 树不为空,按二叉搜索树性质查找插入位置,插入新结点,如key结点值存在,则插入失败。
    在这里插入图片描述
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) {parent = cur;cur = cur->_right;}else if (cur->_key > key) {parent = cur;cur = cur->_left;}else return false;}if (parent->_key < key) parent->_right = new Node(key);else parent->_left = new Node(key);return true;
}

find(查找)

二叉搜索树的查找:

  1. 从根开始比较,比跟大则往右边走查找,比根小则往左边走查找。
  2. 最多查找高度次,走到空,还没找到,这个值不存在。
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;
}

erase(删除)

删除的逻辑相比其他的实现来说复杂很多,二叉搜索树的删除:
首先查找元素是否在二叉搜索树中,如果不存在,则返回;否则要删除结点可能分以下三种情况:

  1. 被查找到的结点无孩子(直接删除)
  2. 被查找到的结点有一个孩子(删除结点,将孩子交给父亲)
  3. 被查找到的结点有两个孩子(在其右孩子中找最左边的孩子(如果此孩子不存在,则为该结点右孩子),用它的值填补到被删除结点中,再来处理增补结点的删除。)相当于找一个合适的子节点替代

在这里插入图片描述

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 {if (cur == _root && cur->_left == nullptr) {_root = cur->_right;delete cur;return true;}else if (cur == _root && cur->_right == nullptr) {_root = cur->_left;delete cur;return true;}if (cur->_left == nullptr) {if (parent->_right == cur)parent->_right = cur->_right;elseparent->_left = cur->_right;delete cur;}else if (cur->_right == nullptr) {if (parent->_right == cur)parent->_right = cur->_left;elseparent->_left = cur->_left;delete cur;}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 == cur)rightMinP->_right = rightMin->_right;elserightMinP->_left = rightMin->_right;delete rightMin;}return true;}}return false;
}

destroy(析构)

二叉树的析构需要传入根结点,通过后序遍历递归实现,但是从外界无法访问对象内部的私有成员_root。所以咱们可以实现一个工具函数,用来帮助完成二叉搜索树的析构。

~BSTree()
{Destroy(_root);_root = nullptr;
}void Destroy(Node* root)
{if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;
}

InOrder(中序遍历)

逻辑跟析构一样。中序遍历下来的key是有序的。

void InOrder()
{_InOrder(_root);std::cout << std::endl;
}void _InOrder(Node* root)
{if (root == nullptr)return;_InOrder(root->_left);std::cout << root->_key << " ";_InOrder(root->_right);
}

拷贝构造

本质上就是实现一次二叉树的深拷贝,也是嵌套了一个递归。

BSTree(const BSTree<K>& t)
{_root = _Copy(t._root);
}Node* _Copy(Node* root)
{if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key);newRoot->_left = _Copy(root->_left);newRoot->_right = _Copy(root->_right);return newRoot;
}

🔥二叉搜索树的应用

像我们刚刚实现的,只存一个数据,是典型的K模型;如果存两个数据,那就是KV模型。

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
  • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
  1. **KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。**该种方式在现实生活中非常常见:
  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

在以上实现K模型的基础上,实现KV模型无非就是让结点多存储一个元素,给模板增添一个类型,具体实现代码如下:

#pragma once
#include<iostream>namespace ForcibleBugMaker
{template<class K, class V>struct BSTreeNode{BSTreeNode<K, V>(const K& k = K(), const V& v = V()):_key(k), _value(v), _left(nullptr), _right(nullptr){}K _key;V _value;BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;};template<class K, class V>class BSTree{typedef BSTreeNode<K, V> Node;public:BSTree() = default;BSTree(const BSTree<K, V>& t){_root = _Copy(t._root);}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;}if (parent->_key < key) parent->_right = new Node(key, value);else parent->_left = new Node(key, value);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 {if (cur == _root && cur->_left == nullptr) {_root = cur->_right;delete cur;return true;}else if (cur == _root && cur->_right == nullptr) {_root = cur->_left;delete cur;return true;}if (cur->_left == nullptr) {if (parent->_right == cur)parent->_right = cur->_right;elseparent->_left = cur->_right;delete cur;}else if (cur->_right == nullptr) {if (parent->_right == cur)parent->_right = cur->_left;elseparent->_left = cur->_left;delete cur;}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 == cur)rightMinP->_right = rightMin->_right;elserightMinP->_left = rightMin->_right;delete rightMin;}return true;}}return false;}~BSTree(){Destroy(_root);_root = nullptr;}void InOrder(){_InOrder(_root);std::cout << std::endl;}private: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;}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);std::cout << root->_key << ":" << root->_value << " ";_InOrder(root->_right);}Node* _root = nullptr;};
}

🔥二叉搜索树的性能

二叉搜索树(Binary Search Tree, BST)的性能主要取决于其结构。理想情况下,二叉搜索树是一个平衡树,其中每个节点的左子树只包含小于节点值的元素,右子树只包含大于节点值的元素,且左、右子树的高度大致相等。然而,在实际应用中,由于插入和删除操作的随机性,二叉搜索树可能会退化为链表状结构(即所有节点都偏向一侧),这会导致其性能急剧下降
在这里插入图片描述
时间复杂度:

  • 搜索(Search):在平衡的二叉搜索树中,搜索操作的时间复杂度为O(log n),其中n是树中节点的数量。这是因为每次递归或迭代都排除了一半的搜索空间。但在最坏的情况下(树退化为链表),时间复杂度会退化为O(n)
  • 插入(Insert)和删除(Delete):同样,在平衡的二叉搜索树中,插入和删除操作的时间复杂度也是O(log n)。但在最坏的情况下,时间复杂度会退化为O(n)

空间复杂度:

  • 二叉搜索树的空间复杂度主要由树中存储的节点数量决定,为O(n)

优化:
为了保持二叉搜索树的平衡,避免性能退化,可以使用各种自平衡二叉搜索树的数据结构,如:

  • AVL树:任何节点的两个子树高度最大差的绝对值小于二,通过旋转操作来维持平衡。
  • 红黑树:一种自平衡二叉查找树,通过特定的操作和性质(如节点着色和树的高度限制)来保持树的平衡。
  • B树和B+树:这些树不仅用于保持数据的排序,还优化了磁盘读写操作,常用于数据库和文件系统中。

这些平衡二叉树也将会是我们未来在数据结构进阶中主要展开的内容。

🌈结语

本篇博客主要讲了二叉搜索树及其实现,K模型和KV模型,最后分析了二叉搜索树的性能,同时介绍了一些维持树平衡的解决方案。感谢大家的支持♥

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

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

相关文章

分布式锁、Lua脚本、redisson、运行lua脚本优化代码

20240721 一、分布式锁1. 什么是分布式锁2. 分布式锁的实现3. 基于redis的分布式锁4 总结 二、对于lua脚本可以保证事务&#xff0c;要么成功要么失败。1. 在redis中调用lua脚本 三、Redisson1 步骤2. Redisson的总结3. 几种分布式锁的区别 三、优化我们的秒杀1. 我们在创建优惠…

Docker安装笔记

1. Mac安装Docker 1.1 Docker安装包下载 1.1.1 阿里云 对于10.10.3以下的用户 推荐使用 对于10.10.3以上的用户 推荐使用 1.1.2 官网下载 系统和芯片选择适合自己的安装包 1.2 镜像加速 【推荐】阿里镜像 登陆后&#xff0c;左侧菜单选中镜像加速器就可以看到你的专属地…

windows和linux的等保加固测评的经验分享

一头等保加固测评的牛马&#xff0c;需要能做到一下午测评n个服务器 接下来就讲讲如何当一头xxxxxxxxx》严肃的等保测评加固的经验分享&#xff08; 一、window等保 首先你要自己按着教程在虚拟机做过一遍&#xff08;win2012和win2008都做过一遍&#xff0c;大概windows的…

pico+unity3d 射线交互教程

前期配置&#xff1a;环境配置参考教程一&#xff0c;手部模型参考教程二&#xff0c;场景基于上一篇搭建。 最终效果&#xff1a;手部射线&#xff08;初始不可见&#xff09;对准 UI 显示&#xff0c;按下手柄 Trigger 键与可交互 UI&#xff08;如 Button、Toggle、Slider …

论文解读 | ICML2024:突破Transformer上下文学习中的瓶颈

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 作者简介 付靖文&#xff0c;西安交通大学博士生 简介 上下文学习&#xff0c;即从上下文示例中学习&#xff0c;是Transformer一项令人印象深刻的能力。然而&#xff0c;由于学习瓶颈的出现——在训练过程中模…

移动UI:任务中心的作用,该如何设计更合理?

任务中心是移动应用中用于展示和管理用户待办任务、提醒事项、用户福利、打卡签到等内容的功能模块。合理设计任务中心可以提升用户体验和工作效率。 以下是一些设计任务中心的合理建议&#xff1a; 1. 易于查看和管理&#xff1a; 任务中心的设计应该使用户能够快速、直观地…

C# 实现跨进程条件变量

C# 进程通信系列 第一章 共享内存 第二章 条件变量&#xff08;本章&#xff09; 第三章 消息队列 文章目录 C# 进程通信系列前言一、关键实现1、用到的主要对象2、初始化区分创建和打开3、变量放到共享内存4、等待和释放逻辑 二、完整代码三、使用示例1、同步控制2、跨进程控…

202496读书笔记|《飞花令·菊(中国文化·古典诗词品鉴)》——荷尽已无擎雨盖,菊残犹有傲霜枝

202496读书笔记|《飞花令菊&#xff08;中国文化古典诗词品鉴&#xff09;》——荷尽已无擎雨盖&#xff0c;菊残犹有傲霜枝 《飞花令菊&#xff08;中国文化古典诗词品鉴&#xff09;》素心落雪 编著。飞花令得名于唐代诗人韩翃《寒食》中的名句“春城无处不飞花”&#xff0c…

KubeSphere核心实战_kubesphere全功能安装_启用kubesphere的热插拔插件---分布式云原生部署架构搭建037

然后我们开始安装kubesphere,首先进入官网点击kubernetes安装 可以看到对应的,条件说kubernetes要在1.20.x以上,我们的是 1.20.9,然后cpu硬件满足,然后,默认存储类型,上一节我们安装好了 然后就可以开始,去下载两个配置文件可以看到上面的两个配置文件 这两个文件,上面是直接…

跨域问题:预检请求

解决跨域问题之预检请求 预检请求&#xff08;Preflight Request&#xff09;是跨域资源共享&#xff08;CORS&#xff09;中用于安全检查的一种机制。 它是由浏览器自动发起的一个OPTIONS请求&#xff0c;目的是在实际跨域请求之前&#xff0c;询问服务器是否允许这次跨域操…

学习TS -类型

let a:number10;//数值型 let b:string"zhaoya";//字符串型 let c:booleantrue;//布尔类型 let d:10;//字面量 let e:any10;//任意类型&#xff0c;可以给别人赋值&#xff0c;而且不会提示报错 let f:unknown50;//未知类型&#xff0c;他给别人赋值&#xff0c;赋值…

kafka开启kerberos和ACL

作者&#xff1a;恩慈 一、部署kafka-KB包 1&#xff0e;上传软件包 依次点击 部署中心----部署组件----上传软件包 选择需要升级的kafka版本并点击确定 2&#xff0e;部署kafka 依次点击部署中心----部署组件----物理/虚拟机部署----选择集群----下一步 选择手动部署-…

消费金融系统开发回忆录

架构设计图 整个支付链路上的功能 支付系统应该有&#xff1a;账户管理、渠道管理、支付管理、对账管理、清算管理、结算管理 一笔支付订单&#xff0c;在支付系统侧就是要记录清楚&#xff0c;谁发起的、对哪个商品进行支付、通过哪个渠道支付、支付时间、支付结果等…

C++基础入门(二)(函数重载,引用,内联函数,nullptr)

目录 一. 函数重载 1. 概念 2. 实现 (1). 参数类型不同 (2). 参数个数不同 (3). 参数类型顺序不同 3. 注意事项 (1). 返回值不能作为重载的条件 (2). 不能仅按函数返回类型重载 (3). 与缺省参数的问题 二. 引用 1. 概念和定义 2. 引用的特性 (1). 引用在定义时必须…

LDR6020双盲插便携显示器应用

随着USB Type-C接口的普及&#xff0c;越来越多的手机和笔记本电脑都支持通过C接口输出视频。这个小巧而精密的接口&#xff0c;大有把传统的HDMI和DisplayPort接口取而代之的架势。特别是usb4的推出&#xff0c;更是为USB TYPE-C接口一统有线接口形态奠定了基础。 单USB-C接口…

python入门课程Pro(1)--数据结构及判断

数据结构及判断 第1课 复杂的多向选择1.if-elif-else2.if嵌套3.练习题&#xff08;1&#xff09;大招来了&#xff08;2&#xff09;奇数还是偶数&#xff08;3&#xff09;简洁代码 第2课 数据与判断小结1.变量2.格式化输出3.逻辑运算-或与非4.判断条件5.练习题&#xff08;1&…

【手撕数据结构】拿捏双向链表

目录 链表介绍初始化链表销毁链表查找节点打印链表增加节点尾插头插在指定位置之后插入节点 删除节点尾删头删删除指定位置节点 链表判空 链表介绍 前面说到&#xff0c;链表的结构一共有八种&#xff1a;带头单向循环链表、带头单向非循环链表、带头双向循环链表、带头双向非…

【初阶数据结构】5.栈和队列

文章目录 1.栈1.1 概念与结构1.2 栈的实现2.队列2.1 概念与结构2.2 队列的实现3.栈和队列算法题3.1 有效的括号3.2 用队列实现栈3.3 用栈实现队列3.4 设计循环队列 1.栈 1.1 概念与结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操…

C语言宏定义格式化控制台打印

写了个简单的控制台打印代码&#xff0c;有三种打印级别 DEBUG INFO ERROR&#xff0c;支持颜色打印&#xff0c;支持时间打印 在MSVC环境中使用 #include <time.h> #include <string.h> #include <stdio.h>/* log level */ #define LOG_LEVEL_DEBUG (1) #d…

【STM32 HAL库】全双工I2S+双缓冲DMA的使用

1、配置I2S 我们的有效数据是32位的&#xff0c;使用飞利浦格式。 2、配置DMA **这里需要注意&#xff1a;**i2s的DR寄存器是16位的&#xff0c;如果需要发送32位的数据&#xff0c;是需要写两次DR寄存器的&#xff0c;所以DMA的外设数据宽度设置16位&#xff0c;而不是32位。…