【C++】详解 set | multiset

目录

1.集合类 set

0.引入

1.set 介绍

1. 构造

2.Insert 插入

3.find 查找

4.count 判断是否在

5.erase 删除

6.lower_bound 和 upper_bound 区间查找

拓展:lower_bound 函数底层实现

equal_range 值区间

2.multiset 类

0.引入:不去重的 set

1.增删查改


1.集合类 set

0.引入

首先要知道的是序列式容器,这种容器我们之前接触过,比如vector,list,deque等等。

序列式容器:

底层为线性的数据结果(物理上或者逻辑上),容器中的元素储存的是元素本身。

而且我们之前在使用序列式容器的时候,插入数据和删除数据只管操作就行,不用考虑其他因素。

关联式容器:

存储的是<key,value>结构的键对值,在数据检索时比序列式容器效率更高。

插入和删除数据时,要考虑该数据和它前后数据之间的关联性。

总的来说,关联式容器存放的数据不同,而且数据前后有一点的关联性

1.set 介绍

通过插入新的元素来扩展容器,有效地通过插入的元素数量增加容器的大小。

因为集合中的元素是唯一的,插入操作检查每个插入的元素是否等同于已经在容器中的元素。

如果是,则不插入该元素,返回一个到这个现有元素的迭代器(如果该函数返回一个值)。

头文件:#include <set>

set和之前学习的容器一样,也是一个模板类,但是它的底层是关联式容器,也就是二叉搜索树,并且有很多的成员函数

1. 构造

构造函数有三个,分别是默认构造函数、使用迭代器区间的构造函数、拷贝构造函数。

因为底层是二叉搜索树,所有就会涉及到比较,构造函数参数就是比较方式,是一个仿函数,但是默认情况下是有缺省值的。

#include<iostream>
#include<set>
using namespace std;
int main()
{int arr[] = { 9,5,1,4,7,6,2,3,5,5,5,5,5 };set<int> s2(arr, arr + sizeof(arr) / sizeof(arr[0]));//迭代器区间构造for (auto& e : s2)//取别名{cout << e << " ";}cout << endl;return 0;
}

迭代器区间构造s2的时候,会发现其有 排序+去重

  1. 数组中有多个重复数字,用这些数字构造出来的set,里面每个数字只存在一个,重复的都被去除了。(如果想知道去重次数 map)
  2. 迭代器的中序移动:在打印set中的数据时,我们发现打印出来的结果是升序的,在学习二叉搜索树的时候我们知道,采用中序遍历的方式打印出来的结果就是升序的。

接下来让我们来学习一下 set 的各种接口

2.Insert 插入

下面我们来简单来一下 𝑆𝑒𝑡 的插入,调用的 insert 接口:

pair<iterator,bool> insert (const value_type& val);
iterator insert (iterator position, const value_type& val);template <class InputIterator>  void insert (InputIterator first, InputIterator last);

测试:

void test_set1() {set<int> s;s.insert(4);s.insert(5);s.insert(2);s.insert(1);s.insert(3);s.insert(3);// 排序 + 去重set<int>::iterator it = s.begin();while (it != s.end()) {cout << *it << " ";++it;}cout << endl;
}int main(void)
{test_set1();return 0;
}

再次验证了 𝑆𝑒𝑡 是 "排序 + 去重" 的!

3.find 查找

下面再介绍一下 find 接口,如果这个元素被找到就会返回 val 的迭代器,否则返回 end

set<int>::iterator pos = s.find(2);//要用迭代器接收

   void test_set2() {set<int> s;s.insert(4);s.insert(5);s.insert(2);s.insert(1);s.insert(3);s.insert(3);set<int>::iterator pos = s.find(2);//要用迭代器接收if (pos != s.end()) {cout << "找到了" << endl;}
}

我们现在用下算法中的 find 接口,看看和 𝑆𝑒𝑡 中的 find 的写法有什么区别?哪个更好:

pos = find(s.begin(), s.end(), 2);
if (pos != s.end()) {cout << "找到了" << endl;
}

algorithm 中的 find 是暴力查找的方式实现的,从 begin 查到 end,其时间复杂度为 O(n),set 的时间复杂度是 o(log N)

4.count 判断是否在

介绍一下 count 接口,有时我们需要判断元素在不在集合中,使用 count 往往比使用 find 方便

count 非常的简单粗暴,如果在集合中则返回1,不在则返回 0

测试:

void test_set3() {set<int> s;s.insert(4);s.insert(5);s.insert(2);s.insert(1);s.insert(3);s.insert(3);if (s.count(3)) {cout << "3在" << endl;}
}

这里如果用 find 就没这么方便了,你还需要做一个判断:

if (s.find(3) != s.end()) {cout << "3在" << endl;
}

5.erase 删除

有三个重载函数,第一个删除指定迭代器的值,第二个返回删除指定值的个数,第三个删除迭代器区间中所有数据。

利用 find 返回迭代器测试第一种:

第二种--直接删除:

思考:那迭代器的方式删除和直接删除有什么区别呢?

💡 解答:迭代器 find + erase(pos) 的是找到了再删,我们一般和 find 搭配使用,因为如果这个元素不存在我们还强硬调用 erase 删除就会引发报错。而 erase(x) 就比较好了,直接删不存在不会报错。

6.lower_bound 和 upper_bound 区间查找

lower_bound() 用于在指定区域内查找 >= 目标值的第一个元素

return 一个指向集合中第一个大于等于给定值的元素的迭代器,如果找不到,则返回 end

即获取集合中任意元素的下界:

[x,+∞)

测试:

对返回的迭代器,解引用打印

如果我们需要删除大于等于 x 的所有元素,我们就可以用到这个接口了。

所以说返回迭代器 都是有妙用的~

还有一个 upper_bound 接口,在指定范围内查找大于目标值的第一个元素,不包含等于。

(x,+∞)

这两个接口就是为了迎合迭代器 erase 的 "左闭右开" 而配备的,因为要是右开,所以是大于的第一个数,没带等号了

💬 代码演示:删除区间 [x,y]

#include <iostream>
#include <set>
using namespace std;int main(void)
{set<int> myset;set<int>::iterator itlow, itup;//auto的快乐就体现啦for (int i = 1; i < 10; i++)myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90myset.insert(35);for (auto e : myset) {cout << e << " ";} cout << endl;//删除[30 60]itlow = myset.lower_bound(30);   // >=  30itup = myset.upper_bound(60);     // >  70myset.erase(itlow,itup);cout << "删除后: ";for (auto e : myset) {cout << e << " ";} cout << endl;return 0;
}

拓展:lower_bound 函数底层实现

这个模板函数 lower_bound 在一个有序区间 [first, last) 中查找第一个不小于给定值 val 的位置。它是一个通用的二分查找算法,适用于任何支持随机访问迭代器的容器,如 vector, deque, array 等。

template <class ForwardIterator, class T>
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val)
{ForwardIterator it;iterator_traits<ForwardIterator>::difference_type count, step;count = distance(first,last);while (count>0){it = first; step=count/2; advance (it,step);if (*it<val) {first=++it;count-=step+1;}else count=step;}return first;
}

代码分解

template <class ForwardIterator, class T>
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val
)

模板参数

  • ForwardIterator:一种迭代器类型,必须支持前向迭代(ForwardIterator)。

  • T:要查找的值的类型。

参数

  • first:指向序列起始位置的迭代器。

  • last:指向序列结束位置的迭代器(不包含在范围内)。

  • val:要查找的值。

函数体

ForwardIterator it;
iterator_traits<ForwardIterator>::difference_type count, step;
count = distance(first, last);
  • it:一个中间迭代器,用于在查找过程中指向当前检查的元素。

  • count:序列中元素的数量,由 distance 函数计算得到。

  • step:步长,用于二分查找过程中每次查找的中间位置。

while (count > 0)
{it = first; step = count / 2; advance(it, step);if (*it < val) {first = ++it;count -= step + 1;}else count = step;
}

这个 while 循环实现了二分查找:

  1. 初始化中间迭代器和步长

  • it = first:将中间迭代器 it 初始化为 first
  • step = count / 2:步长为当前范围的一半。
  • advance(it, step):将 it 向前移动 step 步。
  1. 比较中间值与目标值

    • if (*it < val):如果中间值小于目标值 val,则说明目标值在 it 的右侧。
      • first = ++it:将 first 移动到 it 的下一个位置。
      • count -= step + 1:更新剩余元素的数量。
    • else:否则目标值在 it 的左侧或正好是 it
      • count = step:更新剩余元素的数量。

这个过程会不断缩小查找范围,直到找到第一个不小于 val 的位置或者范围为空。

返回值

return first;

最终返回 first,它指向第一个不小于 val 的位置,如果所有元素都小于 val,则返回 last

总结

lower_bound 函数利用二分查找法在有序序列中找到第一个不小于给定值 val 的位置。通过逐步缩小查找范围,这种方法可以在对数时间复杂度 O(log n) 内完成查找,是一种高效的算法。


equal_range 值区间

equal_range 是 C++ 标准库中的一个函数模板,主要用于在有序容器(如 set, map, vector 等)中查找一个值的范围。它返回一对迭代器,表示要查找值在容器中第一次出现的位置和最后一次出现的位置

template <class ForwardIterator, class T>
std::pair<ForwardIterator, ForwardIterator>
equal_range(ForwardIterator first, ForwardIterator last, const T& val);
  • first:指向容器起始位置的迭代器。
  • last:指向容器结束位置的迭代器。
  • val:要查找的值。

返回值:

  • std::pair<ForwardIterator, ForwardIterator>:包含两个迭代器,第一个迭代器指向第一个不小于 val 的位置,第二个迭代器指向第一个大于 val 的位置。

完善的代码

下面是使用 std::set 进行 equal_range 操作的示例代码,包括对 equal_range 返回值的解释和结果的输出。

#include <iostream>
#include <set>int main() {std::set<int> myset = { 10, 20, 30, 40, 50 };// 使用 equal_range 查找 30 的范围auto ret = myset.equal_range(30);std::set<int>::const_iterator itlow = ret.first;  // 第一个不小于 30 的位置std::set<int>::const_iterator itup = ret.second;  // 第一个大于 30 的位置// 输出区间 [itlow, itup)std::cout << "Range: ";for (auto it = itlow; it != itup; ++it) {std::cout << *it << " ";}std::cout << std::endl;return 0;
}

测试和结果

运行上述代码后,你会得到如下输出:

这表明在集合 myset 中,值为 30 的元素的范围起始于第一个不小于 30 的位置(即 30 本身),结束于第一个大于 30 的位置(即 40)。因此,区间 [itlow, itup) 只包含 30。


2.multiset 类

0.引入:不去重的 set

std::set

  • std::set存储的是不重复的元素,每个元素的键值必须是唯一的。
  • 插入重复的元素时,std::set只保留一个副本。
  • 元素根据其键值排序,通常使用红黑树作为底层数据结构实现,这意味着查找、插入和删除的时间复杂度通常是O(log n)。

std::multiset

  • std::multisetstd::set类似,但它允许存储重复的元素,也就是说,相同的键值可以出现多次
  • 插入重复的元素时,std::multiset保留所有副本
  • std::set一样,std::multiset也保持元素排序,且具有相同的底层数据结构和时间复杂度特征

1.增删查改

insert 插入:

#include <iostream>
#include <set>
using namespace std;void test() {multiset<int> s;s.insert(4);s.insert(5);s.insert(2);s.insert(1);s.insert(3);s.insert(3);// 支持范围 forfor (auto e : s) {cout << e << " ";}cout << endl;
}

erase 删除:

multiset 的 erase 接口会把所有指定元素删掉:

find 查找:

multiset 的 find 如果要查找的元素在集合中有多个,会返回中序的第一个,假设我们要 find(3):

#include <iostream>
#include <set>
using namespace std;int main() {multiset<int> s;s.insert(3);s.insert(5);s.insert(8);s.insert(7);s.insert(7);s.insert(9);s.insert(7);for (auto e : s){cout << e << " ";}cout << endl;// 返回中序第一个7auto pos = s.find(7);while (pos != s.end()){//*pos = 10;cout << *pos << " ";++pos;}cout << endl;return 0;
}

set和multiset都有这三个接口,以前我们也没学过,这里说一下,但是用的很少。

根据下面一段来说一说

int main()
{std::multiset<int> mymultiset;std::multiset<int>::iterator itlow, itup;for (int i = 1; i < 8; i++) mymultiset.insert(i * 10); // 10 20 30 40 50 60 70itlow = mymultiset.lower_bound(30);             itup = mymultiset.upper_bound(40);                       mymultiset.erase(itlow, itup);                    // 10 20 50 60 70std::cout << "mymultiset contains:";for (std::multiset<int>::iterator it = mymultiset.begin(); it != mymultiset.end(); ++it)std::cout << ' ' << *it;std::cout << '\n';return 0;
}

上面就是删掉一段区间。
lower_bound(30);返回大于等于这个值的边界
upper_bound(40);返回大于这个值的边界
所以说这两个接口是为了方便或者左闭右开的区间的。
equal_range(30);是获得这个值的边界。


map 下节预告~

set 都是 const 迭代器不允许被修改,map 可以

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Xlua原理 二

一已经介绍了初步的lua与C#通信的原理&#xff0c;和xlua的LuaEnv的初始化内容。 这边介绍下Wrap文件。 一.Wrap介绍 导入xlua后可以看到会多出上图菜单。 点击后生成一堆wrap文件&#xff0c;这些文件是lua调用C#时进行映射查找用的中间代码。这样就不需要去反射调用节约性…

Anaconda下安装配置Jupyter

Anaconda下安装配置Jupyter 1、安装 conda activate my_env #激活虚拟环境 pip install jupyter #安装 jupyter notebook --generate-config #生成配置文件提示配置文件的位置&#xff1a; Writing default config to: /root/.jupyter/jupyter_notebook_config.py检查版本&am…

Ai绘画变现的14种途径 学习Stablediffusion midjourney用途

AIGC&#xff0c;一个在当代社会中不可忽视的词汇&#xff0c;指的是利用人工智能技术生成创作内容。近年来&#xff0c;全球范围内涌现出50个热门的AI工具&#xff0c;其中&#xff0c;以140亿次访问量雄踞榜首的“GBT”&#xff0c;无疑是AI领域的领头羊。在这些工具中&#…

三、GPIO按键读取

在上一篇文章中&#xff0c;我们详细讲解了GPIO的写函数。万事万物都具有一定的相对性&#xff0c;GPIO的操作也不例外。既然有写操作&#xff0c;那么必然也有读操作。有了上一篇文章的基础&#xff0c;理解本篇内容将会更加容易。 一、这篇文章能了解什么 本篇文章将基于上一…

vue3.0学习笔记(二)——生命周期与响应式数据(ref,reactive,toRef,toRefs函数)

1. 组合API-setup函数 使用细节&#xff1a; setup 是一个新的组件选项&#xff0c;作为组件中使用组合API的起点。从组件生命周期来看&#xff0c;它的执行在组件实例创建之前vue2.x的beforeCreate执行。这就意味着在setup函数中 this 还不是组件实例&#xff0c;this 此时是…

ES中的数据类型学习之Aggregate metric(聚合计算)

Aggregate metric field type | Elasticsearch Guide [7.17] | Elastic 对于object类型的字段来说&#xff0c;可以存子字段为 min/max/sum/value_count PUT my-index {"mappings": {"properties": {"my-agg-metric-field": { -- 字段名"ty…

【测开能力提升-fastapi框架】fastapi能力提升 - 中间件与CORS

1. 中间件 1.1 介绍&#xff08;ChatGPT抄的&#xff0c;大致可以理解&#xff09; 一种机制&#xff0c;用于在处理请求和响应之前对其进行拦截、处理或修改。中间件可以在应用程序的请求处理管道中插入自定义逻辑&#xff0c;以实现一些通用的功能&#xff0c;如身份验证、…

idea部分常用模板

idea部分常用模板 —2020年06月09日 psvm&#xff08;main方法&#xff09; //模板一&#xff1a;psvmpublic static void main(String[] args) {}sout&#xff08;输出&#xff09; //模板二&#xff1a;soutSystem.out.println("hello!");//变形&#xff1a; s…

React中的无状态组件:简约之美

&#x1f389; 博客主页&#xff1a;【剑九 六千里-CSDN博客】 &#x1f3a8; 上一篇文章&#xff1a;【掌握浏览器版本检测&#xff1a;从代码到用户界面】 &#x1f3a0; 系列专栏&#xff1a;【面试题-八股系列】 &#x1f496; 感谢大家点赞&#x1f44d;收藏⭐评论✍ 引言…

前言及汇编(30小时精通C++和外挂实战)

前言及汇编&#xff08;30小时精通C和外挂实战&#xff09; 1&#xff0c;前言2&#xff0c;汇编的重要性&#xff08;1&#xff09;网上流传的谬论&#xff08;2&#xff09;国内技术氛围&#xff08;3&#xff09;学习建议&#xff08;4&#xff09;代码本质挖掘&#xff08;…

C语言之宏详解(超级详细!)

目录 一、用宏前须知-#define相关知识 大致结构&#xff1a; 对预定义符号的补充&#xff1a; 二、用#define定义宏 什么是宏&#xff1f; #define的替换规则&#xff1a; 三、常用的宏定义 1、宏定义常量 2、定义一个宏语句 3、宏定义函数 宏与函数的对比&#xff1a; …

【Linux 驱动】IMX6ULL eLCDIF驱动

1. eLCDIF设备树 lcdif: lcdif021c8000 {compatible "fsl,imx6ul-lcdif", "fsl,imx28-lcdif"; //属性reg <0x021c8000 0x4000>; //起始地址 地址大小interrupts <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>; …

[路由器]IP-MAC的绑定与取消

背景&#xff1a;当公司的网络不想与外部人员进行共享&#xff0c;可以在路由器页面配置IP-MAC的绑定&#xff0c;让公司内部人员的手机和电脑的mac&#xff0c;才能接入到公司。第一步&#xff1a;在ARP防护中&#xff0c;启动IP-MAC绑定选项&#xff0c;必须启动仅允许IP-MAC…

Modbus转BACnet/IP网关快速对接Modbus协议设备与BA系统

摘要 在智能建筑和工业自动化领域&#xff0c;Modbus和BACnet/IP协议的集成应用越来越普遍。BA&#xff08;Building Automation&#xff0c;楼宇自动化&#xff09;系统作为现代建筑的核心&#xff0c;需要高效地处理来自不同协议的设备数据&#xff0c;负责监控和管理建筑内…

双边性:构建神经网络的新方法

正如承诺的那样&#xff0c;这是最近我遇到的最有趣的想法之一的第二部分。如果你错过了&#xff0c;请务必观看本系列的第一部分 - 神经科学家对改进神经网络的看法 - 我们讨论了双边性的生物学基础以及我们大脑的不对称性质如何带来更高的性能。 在这篇文章中&#xff0c;我…

吴恩达深度学习笔记1 Neural Networks and Deep Learning

参考视频&#xff1a;(超爽中英!) 2024公认最好的【吴恩达深度学习】教程&#xff01;附课件代码 Professionalization of Deep Learning_哔哩哔哩_bilibili Neural Networks and Deep Learning 1. 深度学习引言(Introduction to Deep Learning) 2. 神 经 网 络 的 编 程 基 础…

【RT摩拳擦掌】RT600 4路音频同步输入1路TDM输出方案

【RT摩拳擦掌】RT600 4路音频同步输入1路TDM输出方案 一&#xff0c; 文章简介二&#xff0c;硬件平台构建2.1 音频源板2.2 音频收发板2.3 双板硬件连接 三&#xff0c;软件方案与软件实现3.1 方案实现3.2 软件代码实现3.2.1 4路I2S接收3.2.2 I2S DMA pingpong配置3.2.3 音频数…

【BUG】已解决:AttributeError: ‘str‘ object has no attribute ‘read‘

AttributeError: ‘str‘ object has no attribute ‘read‘ 目录 AttributeError: ‘str‘ object has no attribute ‘read‘ 【常见模块错误】 【解决方案】 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998https://bbs.csdn.net/topics/617804998 欢迎来到我的主…

【CSS】容器查询:@container

目录 什么是容器查询如何使用container实际应用场景浏览器支持 什么是容器查询 容器查询是一种CSS特性&#xff0c;允许开发者根据组件所在的容器的大小来应用样式&#xff0c;而不是整个视口的大小。这使得组件能够更加灵活地适应不同的布局环境&#xff0c;而不仅仅是依赖于…

IDEA缓存和索引

IDEA缓存和索引 —2020年06月10日 IntelliJ IDEA首次加载项目的时候。都会创建索引&#xff0c;而创建索引的时间根项目的文件多少成正比。 IntelliJ IDEA的缓存和索引主要是用来加快文件查询&#xff0c;从而加快各种查找、代码提示等操作的速度。 某些特殊情况下&#xf…