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

二叉搜索树(c++版)

前言

在前面我们介绍过二叉树这个数据结构,今天我们更进一步来介绍二叉树的一种在实现中运用的场景——二叉搜索树。二叉搜索树顾名思义其在“搜索”这个场景下有不俗的表现,之所以会这样是因为它在二叉树的基础上添加了一些属性。下面我们就来简单的介绍一下二叉搜索树

概念

二叉搜索树又称搜索二叉树,它可以是一颗空树,如果有节点那么它的任意一个节点都必须满足如下的要求:

该节点的左树为空树或者该节点的值大于等于左树上所有节点的值

该节点的右树为空树或者该节点的值小于等于右树上所有节点的值

该节点的左右子树必须也为二叉搜索树

⼆叉搜索树中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,具体看使⽤场景定义

map/set/multimap/multiset系列容器底层就是⼆叉搜索树,其中map/set不⽀持插⼊相等
值,multimap/multiset⽀持插⼊相等值

性能分析

前面我们将它在搜索的场景下有不俗的表现,下面我们就来简单的看一下它的性能如何

当它接近于平衡二叉树时,搜索的时间复杂的可以达到O(logN),但我们无法保证这一点(这里的无法保证指的是普通的搜索二叉树,暂不包含红黑树等加入调整算法的搜索二叉树)。当我们给搜索二叉树插入一段有序的数据,⼆叉搜索树退化为单⽀树(或者类似单⽀),,此时的时间复杂的就是O(N)

为此我们需要不断的引入调整算法,让这颗二叉树更接近平衡,或者说让它不至于退化,

这就涉及到⼆叉搜索树的变形,平衡⼆叉搜索树AVL树和红⿊树,才能适⽤于我们在内存中存储和搜索数据。这不是我们今天的重点,我们今天只介绍最简单的形态

当然了如果只是想要实现O(logN)的搜索算法,二分法也可以实现,但二分法相比于我们今天介绍的数据结构在某些层面还是不足的,比如二分法想要存储在支持下标随机访问的结构中,二分法在插入删除的场景下天然弱势……

实现

下面我们就来简单的实现一个类似于标准库中set的结构(标准库的搜索二叉树有调整算法,我们就不在这里实现了),不允许存储相同元素(如果需要实现存储相同元素,只需要在插入的逻辑中如果遇到相同元素都放在此相同元素的左树的最右节点或者右树的最左节点)

下面我们边实现边理解其中的逻辑

节点类

我们需要实现一个节点类,这个节点应该包含此节点存储元素的值的信息和此节点左子节点和右子节点的信息

我们实现一个支持泛型的

	template<class K>class BSTreeNode{public:private:K val = K();BSTreeNode<K>* left = nullptr;BSTreeNode<K>* right = nullptr;};

构造函数

		BSTreeNode(const K& val = K(), BSTreeNode* left = nullptr, BSTreeNode* right = nullptr):val(val),left(left),right(right){}

<<重载

实现一个<<运算符重载,让节点支持cout打印

先声明一个友元,因为我们要调用节点类的私有成员

template<class K>friend ostream& operator<<(ostream& cout, BSTreeNode<K>* node);

 实现

template<class K>ostream& operator<<(ostream& cout, BSTreeNode<K>* node){if (node != nullptr)cout << node->key;return cout;}

搜索二叉树类

我们还需要实现一个搜索二叉树的类,这个类会保存根节点的地址,和一些方法

方法我们后面再展开,就先实现框架

这里我们将节点类重命名一下,方便我们后续的使用

template<class K>class BSTree{typedef BSTreeNode<K> Node;
private:Node* root = nullptr;};

声明节点类的友元类

树节点难免会使用到节点类的私有成员,左右节点的地址和节点的值都是需要广泛被使用的,我们直接声明为节点类的友元类

template<class K>friend class BSTree;

构造函数

这里只需要处理root的指向即可,没有指向可以指向nullptr

BSTree(Node* root = nullptr):root(root){}

插入

我们这里实现的插入是不允许重复的,可以将函数返回值设计为bool值来标识是否插入成功。

插入逻辑就是一直比较当前节点的值和插入的值大小,去往正确的子树,直到找到nullptr在nullptr这个位置插入,注意记得标识父节点。遇到相同值返回false插入失败即可

bool insert(const K& x){if (root == nullptr){root = new Node(x);return true;}Node* ptr = root;Node* chi = root;while (chi != nullptr){if (x > chi->val){ptr = chi;chi = chi->right;}else if (x < chi->val){ptr = chi;chi = chi->left;}elsereturn false;}if (x < ptr->val){ptr->left = new Node(x);}else{ptr->right = new Node(x);}return true;}

查找元素

一直比较当前节点的值和插入的值大小,去往正确的子树,直到找到nullptr或者相同的值,返回bool标识是否存在

		bool isExistence(const K& key){Node* cut = this->root;while (cut != nullptr){if (key > cut->val){cut = cut->right;}else if (key < cut->val){cut = cut->left;}elsereturn true;}return false;}

遍历

这里我们实现一个中序遍历,中序遍历即可将二叉树按顺序遍历一边

我们这里使用递归实现

		void midOrder(){_midOrder(this->root);cout << endl;}

封装一个子函数可以保证接口的简洁

		void _midOrder(Node*& root){if (root == nullptr){return;}_midOrder(root->left);cout << root->val << " ";_midOrder(root->right);}

赋值重载和拷贝构造

显然如果要实现将一棵树拷贝/赋值给另一棵树是需要深拷贝的,因为浅拷贝会使这树对象指向同一棵树

注意,树的拷贝不能简单的认为是遍历+值插入,应该包含逻辑结构的拷贝

拷贝构造
BSTree(const BSTree& tree){this->root = assign(tree.root);}

封装一个子函数,这个子函数采用后续遍历,先创建好root节点,在创建好左右子树,再将左右子树串联上根节点

Node* assign(Node* n2){if (n2 == nullptr)return nullptr;Node* n = new Node(n2->key);n->left = assign(n2->left);n->right = assign(n2->right);return n;}
赋值重载

我们使用现代写法,先使用拷贝构造创建临时对象,再将this里的root和临时对象的root交换实现两颗树的交换,离开函数,临时对象会被自动释放

BSTree& operator=(BSTree tree){//swap(*this, tree); swap(this->root, tree.root);return *this;}

注意,两个树的交换我们可以通过交换root实现

析构函数和清除节点

析构函数应该遍历一边这棵树,逐一释放节点,使用中序遍历即可保证释放完左右子树的时候总是可以回到root节点释放root

~BSTree(){clear();}

我们这里还是封装一个函数,这个函数有些用处就不设为子函数,这个函数作用是清除节点

void clear(){_clear(this->root);this->root = nullptr;}

还是使用一个子函数封装

void _clear(Node* root){if (root == nullptr){return;}_clear(root->left);_clear(root->right);delete root;}

删除节点

删除节点还是有些复杂的,复杂在于有些节点是不能直接删除的,直接删除会败坏树的结构,我们采用的思路是替换删除

如果这个节点没有子节点,那么直接删除这个节点,父节点指向这个删除节点一侧的指针置空

如果这个节点有左或者右其中的一个节点,那么父节点指向删除节点一侧的指针指向待删除节点非空子树

如果该节点左右都有节点可以找该节点左树的最右节点或者该节点右树的最左节点覆盖待删除节点的值,将问题转换为删除这个覆盖待删除节点的节点,可以保证这个节点至多只有一棵非空子树

注意如果删除的是根节点特殊处理一下

bool del(const K& key){Node* par = nullptr;Node* chi = this->root;while (chi != nullptr){if (key > chi->val){par = chi;chi = chi->right;}else if (key < chi->val){par = chi;chi = chi->left;}else{if (chi->left == nullptr){if (par == nullptr){root = root->right;}else{if (par->left == chi){par->left = chi->right;}else {par->right = chi->right;}}delete chi;}else if (chi->right == nullptr){if (par == nullptr){root = root->left;}else{if (par->left == chi){par->left = chi->left;}else {par->right = chi->left;}}delete chi;}else{Node* ptr = chi;Node* cut = chi->right;if (cut != nullptr){while (cut->left != nullptr){ptr = cut;cut = cut->left;}chi->val = cut->val;if (ptr->left == cut){ptr->left = cut->right;}else {ptr->right = cut->right;}}else{if (par->left == chi){par->left = ptr->left;}else{par->right = ptr->left;}}delete cut;}return true;}}return false;}

key和key-value

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

除此之外还有key-value的结构

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

key-value二叉搜索树

下面我们实现一下key-value二叉搜索树,逻辑是完全一致的,注意存储的节点升级为key和value都要存储,查找时按key查找,value支持修改。具体细节我们就不在介绍了,可以参考以下代码实现。注意我们可以将这两种二叉树放在不同的命名空间里

namespace key_val
{template<class K, class V>class BSTreeNode{template<class K, class V>friend class BSTree;template<class K, class V>friend ostream& operator<<(ostream& cout, BSTreeNode<K, V>* node);public:BSTreeNode(const K& key = K(), const V& val = V(), BSTreeNode* left = nullptr, BSTreeNode* right = nullptr):key(key),val(val),left(left),right(right){}V getVal(){return this->val;}private:K key = K();V val = V();BSTreeNode<K, V>* left = nullptr;BSTreeNode<K, V>* right = nullptr;};template<class K, class V>ostream& operator<<(ostream& cout, BSTreeNode<K, V>* node){if (node != nullptr)cout << node->key << ':' << node->val;return cout;}template<class K, class V>class BSTree{typedef BSTreeNode<K, V> Node;public:BSTree(Node* root = nullptr):root(root){}~BSTree(){clear();}BSTree(const BSTree& tree){this->root =  assign(tree.root);}BSTree& operator=(BSTree tree){//swap(*this, tree); swap(this->root, tree.root);return *this;}Node* assign( Node* n2){if (n2 == nullptr)return nullptr;Node* n = new Node(n2->key,n2->val);n->left = assign(n2->left);n->right = assign(n2->right);return n;}void clear(){_clear(this->root);}bool insert(const K& key, const V& val){if (root == nullptr){root = new Node(key, val);return true;}Node* ptr = root;Node* chi = root;while (chi != nullptr){if (key > chi->key){ptr = chi;chi = chi->right;}else if (key < chi->key){ptr = chi;chi = chi->left;}else{chi->val = val;return true;}}if (key < ptr->key){ptr->left = new Node(key, val);}else{ptr->right = new Node(key, val);}return true;}void midOrder(){_midOrder(this->root);cout << endl;}Node* isExistence(const K& key){Node* cut = this->root;while (cut != nullptr){if (key > cut->key){cut = cut->right;}else if (key < cut->key){cut = cut->left;}elsereturn cut;}return nullptr;}bool del(const K& key){Node* par = nullptr;Node* chi = this->root;while (chi != nullptr){if (key > chi->key){par = chi;chi = chi->right;}else if (key < chi->key){par = chi;chi = chi->left;}else{if (chi->left == nullptr){if (par == nullptr){root = root->right;}else{if (par->left == chi){par->left = chi->right;}else {par->right = chi->right;}}delete chi;}else if (chi->right == nullptr){if (par == nullptr){root = root->left;}else{if (par->left == chi){par->left = chi->left;}else {par->right = chi->left;}}delete chi;}else{Node* ptr = chi;Node* cut = chi->right;if (cut != nullptr){while (cut->left != nullptr){ptr = cut;cut = cut->left;}chi->key = cut->key;if (ptr->left == cut){ptr->left = cut->right;}else {ptr->right = cut->right;}}else{if (par->left == chi){par->left = ptr->left;}else{par->right = ptr->left;}}delete cut;}return true;}}return false;}private:Node* root = nullptr;void _midOrder(Node* root){if (root == nullptr){return;}_midOrder(root->left);cout << root << " ";_midOrder(root->right);}void _clear(Node* root){if (root == nullptr){return;}_clear(root->left);_clear(root->right);delete root;}};}

结语

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!

相关文章:

二叉搜索树(c++版)

前言 在前面我们介绍过二叉树这个数据结构&#xff0c;今天我们更进一步来介绍二叉树的一种在实现中运用的场景——二叉搜索树。二叉搜索树顾名思义其在“搜索”这个场景下有不俗的表现&#xff0c;之所以会这样是因为它在二叉树的基础上添加了一些属性。下面我们就来简单的介…...

每日1题-7

...

简单实现log记录保存到文本和数据库

简单保存记录到txt&#xff0c;sqlite数据库&#xff0c;以及console监控记录 using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Data.SQLite; using System.IO;namespace NlogFrame {public enum LogType{Tr…...

敏感字段加密 - 华为OD统一考试(E卷)

2024华为OD机试(E卷+D卷+C卷)最新题库【超值优惠】Java/Python/C++合集 题目描述 【敏感字段加密】给定一个由多个命令字组成的命令字符串: 1、字符串长度小于等于127字节,只包含大小写字母,数字,下划线和偶数个双引号; 2、命令字之间以一个或多个下划线 进行分割; 3、可…...

go 安装三方库

go版本 go versiongo version go1.23.1 darwin/arm64安装 redis 库 cd $GOPATH说明&#xff1a; 这里可以改 GOPATH的值 将如下 export 语句写入 ~/.bash_profile 文件中 export GOPATH/Users/goproject然后使其生效 source ~/.bash_profile初始化生成 go.mod 文件 go mod…...

Java 中的 volatile和synchronized和 ReentrantLock区别讲解和案例示范

在 Java 的并发编程中&#xff0c;volatile、synchronized 和 ReentrantLock 是三种常用的同步机制。每种机制都有其独特的特性、优缺点和适用场景。理解它们之间的区别以及在何种情况下使用哪种机制&#xff0c;对提高程序的性能和可靠性至关重要。本文将详细探讨这三种机制的…...

从GDAL中 读取遥感影像的信息

从GDAL提供的实用程序来看&#xff0c;很多程序的后缀都是 .py &#xff0c;这充分地说明了Python语言在GDAL的开发中得到了广泛的应用。 1. 打开已有的GeoTIF文件 下面我们试着读取一个GeoTiff文件的信息。第一步就是打开一个数据集。 >>> from osgeo import gdal…...

算法闭关修炼百题计划(一)

多看优秀的代码一定没有错&#xff0c;此篇博客属于个人学习记录 1.两数之和2.前k个高频元素3.只出现一次的数字4.数组的度5.最佳观光组合6.整数反转7.缺失的第一个正数8.字符串中最多数目的子序列9.k个一组翻转链表10.反转链表II11. 公司命名12.合并区间13.快速排序14.数字中的…...

vue3实现打字机的效果,可以换行

之前看了很多文章,效果是实现了,就是没有自动换行的效果,参考了文章写了一个,先上个效果图,卡顿是因为模仿了卡顿的效果,还是很丝滑的 目录 效果图:代码如下 效果图: ![请添加图片描述](https://i-blog.csdnimg.cn/direct/d8ef33d83dd3441a87d6d033d9e7cafa.gif 代码如下 原…...

【如何学习操作系统】——学会学习的艺术

&#x1f41f;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢编程&#x1fab4; &#x1f421;&#x1f419;个人主页&#x1f947;&#xff1a;Aic山鱼 &#x1f420;WeChat&#xff1a;z7010cyy &#x1f988;系列专栏&#xff1a;&#x1f3de;️ 前端-JS基础专栏✨前…...

stm32 flash无法擦除

通过bushound调试代码发现&#xff0c;当上位机发送命令到模组后flash将不能擦除&#xff0c;通过 HAL_FLASH_GetError&#xff08;&#xff09;函数查找原因是FLASH Programming Sequence error&#xff08;编程顺序错误&#xff09;&#xff0c;解决办法是在解锁后清零标志位…...

Android—ANR日志分析

获取ANR日志&#xff1a; ANR路径&#xff1a;/data/anrADB指令&#xff1a;adb bugreport D:\bugrep.zip ANR日志分析步骤&#xff1a; “main” prio&#xff1a;主线程状态beginning of crash&#xff1a;搜索 crash 相关信息CPU usage from&#xff1a;搜索 cpu 使用信息…...

9.29 LeetCode 3304、3300、3301

思路&#xff1a; ⭐进行无限次操作&#xff0c;但是 k 的取值小于 500 &#xff0c;所以当 word 的长度大于 500 时就可以停止操作进行取值了 如果字符为 ‘z’ &#xff0c;单独处理使其变为 ‘a’ 得到得到操作后的新字符串&#xff0c;和原字符串拼接 class Solution { …...

近万字深入讲解iOS常见锁及线程安全

什么是锁&#xff1f; 在程序中&#xff0c;当多个任务&#xff08;或线程&#xff09;同时访问同一个资源时&#xff0c;比如多个操作同时修改一份数据&#xff0c;可能会导致数据不一致。这时候&#xff0c;我们需要“锁”来确保同一时间只有一个任务能够操作这个数据&#…...

linux创建固定大小的文件夹用于测试

在linux上创建固定大小的文件夹用于测试磁盘空间不足时的应用故障。 实验环境为centos7&#xff0c;有两种简易方法&#xff1a; 一、使用ramdisk 1、创建文件夹 mkdir /var/mytest 2、创建一个1m大小的临时文件 mount none /var/mytest -t tmpfs -o size1m size也可以写…...

大模型学习路线:这会是你见过最全最新的大模型学习路线【2024最新】

大模型学习路线 建议先从主流的Llama开始&#xff0c;然后选用中文的Qwen/Baichuan/ChatGLM&#xff0c;先快速上手体验prompt工程&#xff0c;然后再学习其架构&#xff0c;跑微调脚本 如果要深入学习&#xff0c;建议再按以下步骤&#xff0c;从更基础的GPT和BERT学起&…...

了解云计算工作负载保护的重要性,确保数据和应用程序安全

云计算de小白 云计算技术的快速发展使数据和应用程序安全成为一种关键需求&#xff0c;而不仅仅是一种偏好。随着越来越多的客户公司将业务迁移到云端&#xff0c;保护他们的云工作负载&#xff08;指所有部署的应用程序和服务&#xff09;变得越来越重要。云工作负载保护&…...

Swagger3基本使用

Swagger 课程目标 Swagger简介【了解】 Springboot整合swagger【掌握】 Swagger 常用注解【掌握】 knife4j-Swagger【会用】 一、Swagger3简介 Swagger 是一系列 RESTful API 的工具&#xff0c;通过 Swagger 可以获得项目的⼀种交互式文档&#xff0c;客户端 SDK 的自 动…...

如何借助Java批量操作Excel文件?

最新技术资源&#xff08;建议收藏&#xff09; https://www.grapecity.com.cn/resources/ 前言 | 问题背景 在操作Excel的场景中&#xff0c;通常会有一些针对Excel的批量操作&#xff0c;批量的意思一般有两种&#xff1a; 对批量的Excel文件进行操作。如导入多个Excel文件…...

JUC并发编程_Lock锁

JUC并发编程_Lock锁 1、Lock锁介绍2、主要方法3、与 synchronized 的区别4、Condition 使用示例 1、Lock锁介绍 Java中的 Lock 锁是 java.util.concurrent.locks 包下的一个接口&#xff0c;它提供了比 synchronized 关键字更灵活的锁定机制。 2、主要方法 lock()&#xff1a…...

Unity中的功能解释(数学位置相关和事件)

向量计算 Vector3.Slerp&#xff08;起点坐标&#xff0c;终点坐标&#xff0c;t&#xff09;&#xff0c;可是从起点坐标以一个圆形轨迹到终点坐标&#xff0c;有那么多条轨迹&#xff0c;那怎么办 Vector3.Slerp 进行的是沿球面插值&#xff0c;因此并不是沿着严格的“圆形…...

ElementPlus---Timeline 时间线组件使用示例

介绍 使用ElementPlus时间线组件在后台首页实现通知公告列表展示&#xff0c;使用Vue3开发。 实现代码 Vue3代码 <el-timeline><el-timeline-itemstyle"max-width: 600px"v-for"(activity, index) in activities":key"index":times…...

推荐4款2024年大家都在用的高质量翻译器。

翻译器在我们的生活中有着很重要的作用&#xff0c;不管是我们在学习还是工作&#xff0c;生活娱乐&#xff0c;出国旅游等场合都会派上用场&#xff0c;它是我们解决沟通的障碍&#xff0c;提高阅读效率的好帮手。我自己使用的翻译器有很多&#xff0c;可以给大家列举几款特别…...

Mybatis 返回 Map 对象

一、场景介绍 假设有如下一张学生表&#xff1a; CREATE TABLE student (id int NOT NULL AUTO_INCREMENT COMMENT 主键,name varchar(100) NOT NULL COMMENT 姓名,gender varchar(10) NOT NULL COMMENT 性别,grade int NOT NULL COMMENT 年级,PRIMARY KEY (id) ) ENGINEInnoD…...

Vue3(三)路由基本使用、工作模式(history,hash)、query传参和param传参、props配置、编程式路由导航

文章目录 一、路由的基本使用二、路由器的工作模式三、RouterLink中to的两种写法四、嵌套路由五、路由传参1. query传参2. params传参 六、路由的propos配置七、编程式路由导航 一、路由的基本使用 安装&#xff1a;npm i vue-router 在src/pages文件下&#xff0c;创建三个路…...

TypeScript概念讲解

简单来说&#xff0c;TypeScript 是 JavaScript 的一个超集&#xff0c;支持 ECMAScript 6 标准&#xff1b; TypeScript 由微软开发的自由和开源的编程语言&#xff1b; TypeScript 设计目标是开发大型应用&#xff0c;它可以编译成纯 JavaScript&#xff0c;编译出来的 Jav…...

C++ | Leetcode C++题解之第437题路径总和III

题目&#xff1a; 题解&#xff1a; class Solution { public:unordered_map<long long, int> prefix;int dfs(TreeNode *root, long long curr, int targetSum) {if (!root) {return 0;}int ret 0;curr root->val;if (prefix.count(curr - targetSum)) {ret pref…...

回复《对话损友 2》

回复《对话损友 2》 承蒙贵人挂念&#xff0c;亦感激贵人给予这般交流的契机&#xff08;对话损友 2 -- 回复-CSDN博客&#xff09;。我自身也一直期望能留存些岁月的痕迹&#xff0c;然而却常困惑于不知哪些事物值得铭记&#xff0c;哪些又应被永远忘却。 随着时光流转&#x…...

MySQL - 运维篇

一、日志 1. 错误日志 2. 二进制日志 3. 查询日志 记录了所有的增删改查语句以及DDL语句 4. 慢查询日志 二、主从复制 1. 概述 2. 原理 3. 搭建 三、分库分表 1. 介绍 2. Mycat概述 3. Mycat入门 4. Mycat配置 5. Mycat分片 6. Mycat管理及监控 四、读写分离 1. 介绍 2. 一…...

WebGIS开发及市面上各种二三维GIS开发框架对比分析

GIS前端开发是现代WebGIS应用开发中非常重要的一环&#xff0c;通过前端开发框架&#xff0c;可以实现地图展示、交互、分析等功能。本文将介绍当前市面上常用的GIS前端开发框架&#xff0c;并进行对比分析。 Leaflet Leaflet是一款轻量级的开源地图库&#xff0c;它提供了丰…...