当前位置: 首页 > 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…...

基于大模型的 UI 自动化系统

基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动

一、前言说明 在2011版本的gb28181协议中&#xff0c;拉取视频流只要求udp方式&#xff0c;从2016开始要求新增支持tcp被动和tcp主动两种方式&#xff0c;udp理论上会丢包的&#xff0c;所以实际使用过程可能会出现画面花屏的情况&#xff0c;而tcp肯定不丢包&#xff0c;起码…...

【人工智能】神经网络的优化器optimizer(二):Adagrad自适应学习率优化器

一.自适应梯度算法Adagrad概述 Adagrad&#xff08;Adaptive Gradient Algorithm&#xff09;是一种自适应学习率的优化算法&#xff0c;由Duchi等人在2011年提出。其核心思想是针对不同参数自动调整学习率&#xff0c;适合处理稀疏数据和不同参数梯度差异较大的场景。Adagrad通…...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异&#xff0c;它们的数据同步要求既要保持数据的准确性和一致性&#xff0c;又要处理好性能问题。以下是一些主要的技术要点&#xff1a; 数据结构差异 数据类型差异&#xff…...

使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装

以下是基于 vant-ui&#xff08;适配 Vue2 版本 &#xff09;实现截图中照片上传预览、删除功能&#xff0c;并封装成可复用组件的完整代码&#xff0c;包含样式和逻辑实现&#xff0c;可直接在 Vue2 项目中使用&#xff1a; 1. 封装的图片上传组件 ImageUploader.vue <te…...

爬虫基础学习day2

# 爬虫设计领域 工商&#xff1a;企查查、天眼查短视频&#xff1a;抖音、快手、西瓜 ---> 飞瓜电商&#xff1a;京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空&#xff1a;抓取所有航空公司价格 ---> 去哪儿自媒体&#xff1a;采集自媒体数据进…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

全面解析数据库:从基础概念到前沿应用​

在数字化时代&#xff0c;数据已成为企业和社会发展的核心资产&#xff0c;而数据库作为存储、管理和处理数据的关键工具&#xff0c;在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理&#xff0c;到社交网络的用户数据存储&#xff0c;再到金融行业的交易记录处理&a…...

论文阅读:Matting by Generation

今天介绍一篇关于 matting 抠图的文章&#xff0c;抠图也算是计算机视觉里面非常经典的一个任务了。从早期的经典算法到如今的深度学习算法&#xff0c;已经有很多的工作和这个任务相关。这两年 diffusion 模型很火&#xff0c;大家又开始用 diffusion 模型做各种 CV 任务了&am…...