【C++详解】STL-set和map的介绍和使用样例、pair类型介绍、序列式容器和关联式容器

文章目录


一、序列式容器和关联式容器

前⾯我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,⽐如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。

关联式容器也是⽤来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是⾮线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。

本节讲解的map和set底层是红⿊树,红⿊树是⼀颗平衡⼆叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。

map和set都不支持单独一个变量值的构造函数,要么创建一个空的map或set,挨个插入变量,要么用initializer_list构造。

二、set系列的使用

set类的介绍

set的声明如下,T就是set底层关键字的类型。

cpp 复制代码
template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type

> class set;
  • set默认要求T⽀持⼩于⽐较,如果不⽀持或者想按⾃⼰的需求⾛可以⾃⾏实现仿函数传给第⼆个模版参数
  • set底层存储数据的内存是从空间配置器申请的,如果需要可以⾃⼰实现内存池,传给第三个参数。⼀般情况下,我们都不需要传后两个模版参数。
  • set底层是⽤红⿊树实现,增删查效率是O(logN) ,迭代器遍历是⾛的搜索树的中序,所以是有序的。
  • 前⾯部分我们已经学习了vector/list等容器的使⽤,STL容器接⼝设计,⾼度相似,所以这⾥我们就不再⼀个接⼝⼀个接⼝的介绍,挑⽐较重要的接⼝进⾏介绍。
    仿函数less< T >默认是左边比根小,右边比根大,我们更爱仿函数为Greator< T >后不会影响查找逻辑,因为查找是的判断也要依赖仿函数的。

set的构造和迭代器

set的⽀持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历走的中序;⽀持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,因为修改关键字数据,破坏了底层搜索树的结构。

使用举例:

cpp 复制代码
int main()
{
	// 去重+升序排序
	set<int> s = { 12, 6, 4, 8 };
	// 去重+降序排序(给⼀个⼤于的仿函数)
	//set<int, greater<int>> s;
	s.insert(5);
	s.insert(2);
	s.insert(7);
	s.insert(5);
	//set<int>::iterator it = s.begin();
	auto it = s.begin();
	while (it != s.end())
	{
		// error C3892: "it": 不能给常量赋值
        // *it = 1;
		cout << *it << " ";
		++it;
	}
	cout << endl;

	// 插⼊⼀段initializer_list列表值,已经存在的值插⼊失败
	s.insert({ 2,8,3,9 });
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	set<string> strset = { "sort", "insert", "add" };
	// 遍历string⽐较ascll码⼤⼩顺序遍历的
	for (auto& e : strset)
	{
		cout << e << " ";
	}
	cout << endl;
}

set的增删查

find:

既然算法库已经有find了,为什么set里还要实现find呢?原因是算法库的find是暴力查找,不管什么容器都是从头到尾遍历一遍,效率很低,而set里的find是利用了搜索树的结构特点,从根节点开始查找,效率更高。

find返回值还是和以前一样,找到了返回指定位置迭代器,没找到返回end()。

cpp 复制代码
// 直接删除x
int x;
cin >> x;
int num = s.erase(x);
if (num == 0)
{
	cout << x << "不存在!" << endl;
}
for (auto e : s)
{
	cout << e << " ";
}
cout << endl;

// 直接查找在利⽤迭代器删除x
cin >> x;
auto pos = s.find(x);
if (pos != s.end())
{
	s.erase(pos);
}
else
{
	cout << x << "不存在!" << endl;
}

for (auto e : s)
{
	cout << e << " ";
}
cout << endl;

如果只想单纯判断某个值在不在,用count更方便:

cpp 复制代码
cin >> x;
if (s.count(x))
{
	cout << x << "在!" << endl;
}
else
{
	cout << x << "不存在!" << endl;
}

erase:

erase可以指定key删除,也可以指定某个迭代器删除。注意三种erase返回值的区别:

删单个元素 / 区间 → 返回 "下一个迭代器",方便接续遍历。

删值 → 返回删除个数,方便判断结果。

因为这里需要结合multset使用,因为multset支持重复键值,所以当multset调用erase它会一次性把容器里等于指定值的元素都删除。

cpp 复制代码
set<int> s = { 4,2,7,2,8,5,9 };
for (auto e : s)
{
	cout << e << " ";
}
cout << endl;

// 删除最⼩值
s.erase(s.begin());
for (auto e : s)
{
	cout << e << " ";
}
cout << endl;

lower_bound/upper_bound


这两个接口一般配合使用,用法如下:

auto it1 = lower_bound(val):找第一个 ≥val 的元素迭代器。

auto it2 = upper_bound(val):找第一个 >val 的元素迭代器。

erase(it1, it2):删除 [it1, it2) 区间的元素,左闭右开。

cpp 复制代码
	std::set<int> myset = { 10, 20, 30, 40, 50, 60, 70, 80, 90 };

	for (auto e : myset)
	{
		cout << e << " ";
	}
	cout << endl;
	// 实现查找到的[itlow,itup)包含[30, 60]区间
	// 返回 >= 30
	auto itlow = myset.lower_bound(30);
	// 返回 > 60
	auto itup = myset.upper_bound(60);
	// 删除这段区间的值
	myset.erase(itlow, itup);
	for (auto e : myset)
	{
		cout << e << " ";
	}
	cout << endl;

结果:

multiset和set的差异

multiset和set的使⽤基本完全类似,主要区别点在于multiset⽀持值冗余,那么

insert/find/count/erase都围绕着⽀持值冗余有所差异,具体参看下⾯的样例代码理解。

(find查找中序的第⼀个值是为了方便把后面相同的值一并找到)

cpp 复制代码
int main()
{
	// 相⽐set不同的是,multiset是排序,但是不去重
	multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };
	auto it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	
	// 相⽐set不同的是,x可能会存在多个,find查找中序的第⼀个
	int x;
	cin >> x;
	auto pos = s.find(x);
	while (pos != s.end() && *pos == x)
	{
		cout << *pos << " ";
		++pos;
	}
	cout << endl;
	
	// 相⽐set不同的是,count会返回x的实际个数
	cout << s.count(x) << endl;
	// 相⽐set不同的是,erase给值时会删除所有的x
	s.erase(x);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

三、map系列的使用

map类的介绍

map就是小编在二次搜索树介绍的key/value版本。map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key⽀持⼩于⽐较,如果不⽀持或者需要的话可以⾃⾏实现仿函数传给第⼆个模版参数,map底层存储数据的内存是从空间配置器申请的。⼀般情况下,我们都不需要传后两个模版参数。map底层是⽤红⿊树实现,增删查改效率是 O(logN) ,迭代器遍历是⾛的中序,所以是按key有序顺序遍历的。

cpp 复制代码
template < class Key, // map::key_type
class T, // map::mapped_type
class Compare = less<Key>, // map::key_compare
class Alloc = allocator<pair<const Key,T> > //
map::allocator_type

> class map;

pair类型介绍

pair是专门为map准备的一个类模板。

map底层的红⿊树节点中的数据,使⽤pair<Key, Value>存储键值对数据。所以map里存的值本质就是pair。pair定义如下:

(最后的拷贝构造函数允许从不同类型的pair<U, V>构造当前pair<T1, T2>,前提是U类型可以转换为T1类型,V类型可以转换为T2类型。)

cpp 复制代码
template <class T1, class T2>
struct pair
{
	//默认构造
	pair() : first(T1()), second(T2())
	{}
	//带参构造
	pair(const T1& a, const T2& b) : first(a), second(b)
	{}
	//泛化的拷贝构造函数
	template<class U, class V>
	pair(const pair<U, V>& pr) : first(pr.first), second(pr.second)
	{}

	//类型重定义
	typedef T1 first_type;
	typedef T2 second_type;
	//成员变量
	T1 first;
	T2 second;
};

pair没有重载operator<<,所以要打印map值只能取pair的成员依次打印,如下所示:

cpp 复制代码
    //范围for
	for (auto e : dict)
	{
		cout << e.first << " " << e.second << endl;
	}
	//迭代器
auto it = dict.begin();
while(it != dict.end())
{
	cout << (*it).first << " " << (*it).second << endl;
	cout << it->first << " " << it->second << endl;
	it++;
}

这里迭代器示例可以反向说明为什么map要用pair来存储值,而不是每个结点直接存两个值一个key一个value,这样方便迭代器解引用取里面的值,如果存两个值迭代器解引用无法返回两个值(标准不支持),若想返回两个值或者多个值必须要用一个结构包装起来整体返回。

map的构造及遍历

map的构造小编就只介绍一下initializer_list构造,如下所示,内层花括号是隐式类型转换,外层花括号代表initializer_list。

cpp 复制代码
map<string, string> dict = { {"left", "左边"}, {"right", "右边"},{"insert", "插⼊"},{ "string", "字符串" } };

范围for遍历时最好加上&,如果遍历时不会修改值最好再把const加上。

cpp 复制代码
// 范围for遍历
for (const auto& e : dict)
{
	cout << e.first << ":" << e.second << endl;
}
cout << endl;

map的增删查

在介绍map接口之前小编先说明一点,map的value_type不再是单纯的key的类型了,而是pair,因为插入操作需要将 key/value 整体插入,也就是这里的pair。


insert:

插入也和set一样,不会插入重复的key,就算key的value不相同也不会重复插入key。

插入我们有四种方法,前三种是C++98支持的,一种是有名对象,一种是匿名对象,还有一种是make_pair函数模板,不用我们显示写数据类型,它可以自动推导类型。(它是内联函数,和匿名对象一样,不会有额外开销)

最后一种C++11是依靠多参数隐式类型转换实现的,因为C++11才开始支持多参数隐式类型转换,之前只有单参数隐式类型转换。
make_pair:

cpp 复制代码
template <class T1,class T2>
inline pair<T1,T2> make_pair (T1 x, T2 y)
{
return ( pair<T1,T2>(x,y) );
}

示例:

cpp 复制代码
map<string, string> dict;
//有名对象
pair<string, string> kv1("sort", "排序");
dict.insert(kv1);
//匿名对象
dict.insert(pair<string, string>("string", "字符串"));
//make_pair
dict.insert(make_pair("array", "数组"));
//多参数隐式类型转换
dict.insert({"list", "链表"});

erase:

erase和find都只能指定key操作,例子和set差不多,小编就不做演示了。


find:

map的[]功能样例

利⽤find和iterator修改功能,统计⽔果出现的次数:

cpp 复制代码
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto e : arr)
{
	auto it = countMap.find(e);
	if (it == countMap.end())
	{
		countMap.insert({e, 1});
	}
	else
	{
		it->second++;
	}
}

for (const auto& e : countMap)
{
    cout << e.first << ":" << e.second << endl;
}
cout << endl;

这里用operator[ ]也可以实现一样的效果,但是这里的operator[ ]显然和之前的容器介绍的区别很大了,跟着小编一起来了解一下map的operator[ ]吧。

cpp 复制代码
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto e : arr)
{
	countMap[e]++;
}

for (const auto& e : countMap)
{
    cout << e.first << ":" << e.second << endl;
}
cout << endl;

我们看文档可以知道它的功能是拿到key的值,返回value值的引用,所以简单来说就是查找+修改。

当value不存在时就会插入这个不存在的值,那么这是怎么做到的呢?原因就是operator[ ]底层会去调用insert,看上面文档小编用红色方框圈起来的部分。所以我们现在的首要目标是先把insert的返回值搞明白。

首先明确一点insert插入的是pair对象。然后看insert的返回值,它返回是是迭代器和bool值构成的pair,当新拆的key在map里没有则插入成功返回true,iterator返回新插入进来的元素的迭代器,否则插入失败返回false,iterator返回已经在map里存在的元素的迭代器。
我们理解了insert过后再来尝试理解operator[ ]是实现细节。operator[ ]一开始先insert,insert需要传key和value,但是operator[ ]的参数没有value的值,所以只能传value 类型的默认构造V(),类似于缺省值。(内置类型也有默认构造)

再看第二行代码,不管key在map里存不存在,反正insert返回的pair里的iterator都会指向key所在的结点,(因为不存在会插入含key的新节点,存在直接返回含key的结点)这时候我们再取insert返回值里的iterator(ret.first)就得到了指向key所在的结点的迭代器,然后再通过迭代器访问结点里pair的第二个成员(ret.first->second)就成功实现了operator[ ]的功能。(返回值这里的first和second分属两个不同的pair)

下面小编写的代码只是把operator[]是大致功能展示了一下,真正的实现细节等小编介绍到map的底层实现时再细讲。

cpp 复制代码
V& operator[](const K& key)
{
	pair<iterator, bool> ret = insert({ key, V() });
	//ret.first返回key所在的迭代器,然后->second返回key所在迭代器里的value
	return ret.first->second; 
}

下面小编展示几个使用operator[ ]的例子:

cpp 复制代码
map<string, string> dict;
//插入
dict["left"];
//插入+修改
dict["right"] = "右边";
//修改
dict["left"] = "左边";
//查找
cout << dict["left"] << endl;

所以这个时候我们再回过去理解operator[ ]统计水果是不是就非常直观了,当map里没有对应水果就是插入+修改,当map里有就只是修改。

multimap和map的差异

multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如find时,有多个key,返回中序第⼀个。还有两点不同就是multimap不⽀持[],因为⽀持key冗余,无法确定[]返回哪个key。其次find的返回值只有iterator了,因为允许重复插入就没有插入成功与否的概念了。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
半桔5 分钟前
【STL源码剖析】从源码看 vector:底层扩容逻辑与内存复用机制
java·开发语言·c++·容器·stl
lly20240613 分钟前
PHP MySQLi 教程:基础与高级应用
开发语言
Shun_Tianyou17 分钟前
Python Day21 re模块正则表达式 简单小说爬取 及例题分析
开发语言·数据结构·python·算法·正则表达式
千里镜宵烛35 分钟前
互斥锁与条件变量
linux·开发语言·c++·算法·系统架构
爱科研的瞌睡虫44 分钟前
C++线程中 detach() 和 join() 的区别
java·c++·算法
每天的每一天1 小时前
分布式文件系统05-生产级中间件的Java网络通信技术深度优化
java·开发语言·中间件
欧的曼1 小时前
cygwin+php教程(swoole扩展+redis扩展)
开发语言·redis·后端·mysql·nginx·php·swoole
智江鹏1 小时前
Android 之 Kotlin中的符号
android·开发语言·kotlin
凤年徐1 小时前
【数据结构与算法】刷题篇——环形链表的约瑟夫问题
c语言·数据结构·c++·算法·链表
牟同學2 小时前
深入理解 C++ 中的stdpriority_queue:从原理到实战的高效优先级管理
数据结构·c++·priority_queue