map和set的使用

文章目录

(一)set系列的使用

1.set的介绍

set是一个关联式容器,关联式容器逻辑结构通常是非线性结构,两个位置之间右紧密的联系,交换一下,它的存储结构就被破坏。set的底层是红黑树,红黑是是一棵平衡的二叉搜索树,set是key搜索场景的结构

set声明的结构如下图:

可以看到它有三个模板参数,它们各自的作用为:

  1. 第一个模板参数为关键字的类型,也就是key的类型
  2. 第二个模板参数是仿函数,用来控制关键字的比较方式,这里默认支持小于的比较方式,若不满足自己的需求,可自己实现一个仿函数
  3. 第三个模板参数是一个空间配置器,用来控制申请结点,增加效率,同样地,若不满足自己的需求,可以自己实现一个内存池

set增删查的效率为O(logN),迭代器遍历是走的搜索树的中序,所以获取到的数据是有序的。一般情况下,最后不需要传后面两个模板参数

2.set的构造和迭代器

set的构造有好几种,例如,无参的默认构造,迭代器构造,拷贝构造,initializer list构造(也就是用一段数组进行构造)

代码举例如下:

cpp 复制代码
#include <iostream>
#include <set>
#include <vector>
using namespace std;

int main()
{
	set<int> s1; //无参的默认构造,int就是我们传的第一个模板参数,后面两个模板参数不需要传
	//再对s1进行数据的插入即可
	s1.insert(2);
	s1.insert(5);
	s1.insert(7);

	//initializer list构造
	set<int> s2 = {8,3,10,2,6,5,13,14};

	//拷贝构造
	set<int> s3 = s2;

	//用迭代器构造
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	set<int> s4(v1.begin(),v1.end());
	return 0;
}

set支持正向和反向迭代器,下面代码是正向迭代器的举例:

cpp 复制代码
#include<iostream>
#include<set>
using namespace std;
int main()
{
	set<int> s1;
	s1.insert(8);
	s1.insert(5);
	s1.insert(10);

	//用迭代器对s1进行遍历
	set<int>::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

结果:

从代码及结果可以看到,set默认按升序的顺序进行遍历的,因为底层结构是搜索二叉树,所以迭代器的遍历是走的中序遍历,set的iterator和const_iterator都不支持迭代器修改数据,修改了关键字的数据,就破坏了二叉搜索树的结构

若想输出结果变为倒序,在构造时修改一下第二个模板参数即可,如下代码:

cpp 复制代码
set<int,greater<int>> s1;//使数据的存储规则变为大的在左,小的在右,迭代器中序遍历时就是倒序了

支持迭代器就说明支持范围for,使用如下:

cpp 复制代码
int main()
{
	set<int> s1;
	s1.insert(8);
	s1.insert(5);
	s1.insert(10);

	for (auto e : s1)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

set容器还有一个功能就是去重,set是不支持值冗余的,也就是说如果要插入的值,已经存在,那么就会插入失败,证明代码如下:

cpp 复制代码
int main()
{
	set<int> s;
	s.insert(3);
	s.insert(2);
	s.insert(5);
	s.insert(5);

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

输出结果如下:

图中只出现了一个5,说明还有一个5插入失败,证明了set容器不支持值冗余,若想要支持值冗余可以用multiset这个容器,该容器与set容器类似,区别就在于是否支持值冗余

3.set的增删查接口

  • 先来看增这个接口如何使用,来看下面这张图片:

    insert函数就是set增的接口,也就是插入数据的接口,红色圈起来的部分就是比较常用的插入方式,用法说明如下:

1.第一个红圈的插入方式是,一个数据一个数据的插入

2.第二个红圈的插入方式是,插入一段迭代器区间

3.第一个红圈的插入方式是,插入一组被花括号括起来的数

4.蓝色圈的插入方式是,在某个位置进行插入,但是若插入的位置破坏了二叉搜索树的规则,编译器就会按照二叉搜索树的规则来进行插入数据,所以一般不会去使用

以上就是比较常用的插入方式,代码举例如下:

cpp 复制代码
#include<iostream>
#include<vector>
#include<set>
int main()
{
	//插入一个个数据
	set<int> s1;
	s1.insert(8);
	s1.insert(3);
	s1.insert(9);
	s1.insert(6);

	//插入一段迭代器区间
	vector<int> v1 = { 3,4,2,1,7,0 };
	set<int> s2;
	s2.insert(v1.begin(), v1.end());

	//插入initializer list,也就是插入一组用花括号括起来的数据
	set<string> s3
	s3.insert({"set","insert","left"});//插入时,同样也会去重
	
	return 0;
}
  • 再来看删这个接口,使用该接口的方式如下图片:

1.第一个方式支持删除某个位置的值

2.第二个方式支持给一个值,直接进行删除操作,返回值是删除的元素的个数,也就是说,若删除成功就返回1,不成功就返回0

3.第三个方式支持删除一段迭代区间的值

代码举例如下:

cpp 复制代码
int main()
{
	set<int> s1 = { 8,3,10,2,6,9 };
	for (auto e : s1)
	{
		cout << e << " ";
	}
	cout << endl;

	//第一个用法:删除某个位置(position)的值,例如删除最小值
	s1.erase(s1.begin());//迭代器走的中序遍历,所以中序的第一个就是最小值
	cout << "删除最小值:";
	for (auto e : s1)
	{
		cout << e << " ";
	}
	cout << endl;

	//第二个用法:给一个值直接进行删除
	int x = 0;
	cin >> x;
	
	if (s1.erase(x) == 0)
	{
		cout << x << "不存在" << endl;
	}
	else
	{
		cout << "删除" << x << "值:";
		for (auto e : s1)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	cout << endl;

	//第三个用法:删除一段迭代区间的值
	set<int> s2 = { 3,1,6,0,7,2 };
	for (auto e : s2)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "删除一段区间的值:";
	s2.erase(s2.begin(), s2.end());
	for (auto e : s2)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

输出结果:

  • 最后来看查这个使用接口,对于查找这个接口来说,其实算法库中也实现了一个查找的接口,那为什么set还要自己实现一个查找的接口呢?

这个是算法库中的find接口:

这个是set的find接口:

我们知道算法库中的find接口,不止可用于set这个容器,它还可以用于vector、list等等其他序列式容器,所以它是一个通用型接口,为了兼容其他容器,它的查找方式是一个个数据遍历去查找,时间复杂度为O(N),效率较低;而set容器中的查找方式是根据二叉搜索树的规则来进行查找的,最差的情况下是查找的树的高度次,时间复杂度为O(logN),查找的效率较高,这就是set自己实现查找接口的原因。

算法库和set的查找接口的使用方式如下:

cpp 复制代码
#include <algorithm>
int main()
{
	set<int> s1 = { 8,3,10,3,2,14 };

	//算法库中的find,时间复杂度为O(N)
	int x;
	cin >> x;
	set<int>::iterator ret1 = find(s1.begin(), s1.end(), x);
	cout << *ret1 << endl;

	//set中的find,时间复杂度为O(logN)
	set<int>::iterator ret2 = s1.find(14);
	//若找到返回值指向找到的元素,否则指向std::end
	return 0;
}

还有另一种快速查找的方法,就是利用set容器中count这个接口

功能:计算某个值在树中出现的次数,返回值就是出现的次数。若该值不在树中就会返回0,否则返回非0,即可以利用它来进行快速查找,代码举例如下:

cpp 复制代码
int main()
{
	//利用count来进行快速查找
	set<int> s = { 8,3,10,6,2,14 };
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
	int x = 0;
	cin >> x;
	if (s.count(x))
	{
		cout << x << "存在!" << endl;
	}
	else
	{
		cout << x << "不存在!" << endl;
	}
	return 0;
}

4.lower_bound和upper_bound接口的使用

这两个接口的功能就是配合起来找一段区间,两个接口的声明如下:

lower_bound接口的声明:

upper_bound接口的声明:

这两个函数就是找某个值的边界。例如,在某一段有序的数中,查找某一段区间[x,y],lower_bound函数负责找左区间x的边界,upper_bound函数负责找右区间y值的边界。那么它们查找的规则是如何的呢?来看下面的一段代码:

cpp 复制代码
int main()
{
	set<int> s = { 10,20,30,40,50,60,70,80,90 };
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	//实现查找[30,60]区间的值
	auto lower = s.lower_bound(25);
	//lower_bound会查找>=30的值,返回结果30

	auto upper = s.upper_bound(65);
	//upper_bound会查找>60的值,返回结果70

	//找到区间之后进行删除
	s.erase(lower, upper);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

结果如下:

从上面的代码中,我们会有疑问,为什么lower_bound函数会找>=左区间的值呢,代码中的数组有30这个值,那如果没有30这个值,那在30到60区间的左区间的边界是不是就应该大于30,所以lower_bound函数会找>=左区间的值;而upper_bound函数找>右区间,而不是>=右区间的值,是因为我们知道,迭代器它是遵循左闭右开这个规则的,若找等于右区间的值,迭代器在遍历的过程中就不会包含右区间,那么60就不会包括在查找到的区间内,若找大于右区间的值,70的话,迭代器就会遍历到60这个数。

5.multiset和set的差异

前面已经提到过multiset了,它们之间最大的差异就是multiset支持值冗余,那么它的增删查接口都会围绕着值冗余的特点来进行实现

  1. multiset的insert功能,因为multiset支持值冗余,所以在插入数据时,就不会进行去重的操作,代码如下:
cpp 复制代码
int main()
{
	//支持值冗余,排序,但不进行去重操作
	multiset<int> mset;
	mset.insert(8);
	mset.insert(3);
	mset.insert(10);
	mset.insert(2);
	mset.insert(3);
	multiset<int>::iterator it = mset.begin();
	while (it != mset.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

结果如下:

2.multiset的find功能,multiset在查找的过程中,可能会出现多个相同的值,例如,如果要查找的值为3,可能就会有多个3,那么就会产生歧义。为解决这个问题,它查找的规则是找中序的第一个,也就是说,找中序遍历的第一个3,下图举例:

证明代码如下:

cpp 复制代码
int main()
{
	multiset<int> mset;
	mset.insert({ 8,3,10,2,4,11,9,4,7,4,1,4 });
	for (auto e : mset)
	{
		cout << e << " ";
	}
	cout << endl;

	int x = 0;
	cin >> x;
	auto pos = mset.find(x); // 找到中序的第一个x
	while (pos != mset.end() && *pos == x)
	{
		cout << *pos << " ";
		++pos; //接着找后面的x,并打印出来
	}

	return 0;
}

输出结果:

  1. multiset的count接口

count在set中是判断某个值在或不在,但在multiset中是计算,某个值的个数,代码如下:

cpp 复制代码
int main()
{
	multiset<int> mset;
	mset.insert({ 8,3,10,2,4,11,9,4,7,4,1,4 });
	for (auto e : mset)
	{
		cout << e << " ";
	}
	cout << endl;

	int x = 0;
	cin >> x;
	size_t ret = mset.count(x);
	cout << "个数:" << ret << endl;
	return 0;
}
  1. multiset的erase功能,是将要删除的值全部删完,不是只删除一个

代码如下:

cpp 复制代码
int main()
{
	multiset<int> mset = { 8,3,10,2,4,11,9,4,7,4,1,4 };
	for (auto e : mset)
	{
		cout << e << " ";
	}
	cout << endl;
	int x = 0;
	cin >> x;
	mset.erase(x);
	for (auto e : mset)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

(二)map系列的使用

1.map的介绍

map也是一个关联式容器,底层也是一棵平衡的二叉搜索树,map是key/value搜索场景的结构

,map声明的结构如下:

cpp 复制代码
template<class key,
	class T,
	class Compare = less<key>,
	class Alloc = allocator<pair<const key, T>>
	>class map;

key是map底层关键字的类型,T是底层value的类型,后面两个模板参数与set后面两个相同,这里不再做介绍,一般情况下,后面两个模板参数也是不需要传的。map增删查改的效率是O(logN),迭代器遍历走中序,所以是按key有序顺序来进行遍历的。

2.pair类型介绍

在map的底层中,存储key和value的数据不是分开存储的,而是用一个pair的类模板来进行存储的

cpp 复制代码
template<class T1,class T2>
struct pair
{
	typedef T1 first_type;
	typedef T2 second_type;

	T1 _first; //用来存放key的数据
	T2 _second;//用来存放value的数据

	pair()
		:_first(T1())
		,_second(T2())
	{}
	//....
};

所以这个key/value这个键值对都是存放在这个pair里面的

3.map的构造和迭代器

  1. map的构造,我们关注以下几个常用的接口即可:

无参构造:

cpp 复制代码
map(const key_compare& comp = key_compare(),
	const allocator_type& alloc = allocator_type());

拷贝构造:

cpp 复制代码
map(const map& x);

initializer list 列表构造:

cpp 复制代码
map(initializer_list<value_type> il
	,const key_compare& comp = key_compare(),
	const allocator_type& alloc = allocator_type());

迭代器区间构造:

cpp 复制代码
template<class InputIterator>
map(InputIterator first,InputIterator last,
	const key_compare& comp = key_compare(),
	const allocator_type& = allocator_type());

举例代码如下:

cpp 复制代码
#include<iostream>
#include<map>

int main()
{
	//无参构造
	map<string, int> mymap1;

	//拷贝构造
	map<string, int> mymap2 = mymap1;

	//initializer list构造
	map<string, int> mymap3 = { {"left",1},{"sort",2},{"insert",3} };

	//迭代器区间构造
	map<string, int> mymap4(mymap3.begin(), mymap3.end());
	
	return 0;
}
  1. map的迭代器,是一个双向迭代器
cpp 复制代码
//正向迭代器:
iterator begin();
iterator end();

//反向迭代器:
reverse_iterator rbegin();
reverse_iterator rend();

迭代器的使用就不举例了,当然支持迭代器也会支持范围for

4.map的增删查改

  1. map的增

因为在map底层中存储key和value的是pair类模板,所以在使用插入的接口时,插入的是pair的键值对数据,跟set是有所不同的

map增接口也就是insert函数,使用方式如下:

cpp 复制代码
pair<iterator,bool> insert (const value_type& val);//支持插入一个pair

template <class InputIterator>
  void insert (InputIterator first, InputIterator last);//支持插入一段迭代区间

void insert (initializer_list<value_type> il);//插入initializer list

//还可以用一个函数模板来进行插入
template<class T1,class T2>
inline make_pair<T1,T2>	make_pair(T1 x,T2 y)
{
	return (pair<T1,T2>(x,y));
}
//make_pair是一个函数模板,只需要传两个参数,函数模板就会自己推导出参数的类型,推导出来之后,再在里面构造一个匿名对象,再返回,相当于又套了一层,本质上还是构造一个匿名对象

举例代码如下:

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;
int main()
{
	//插入一个pair
	pair<string, string> pv1 = { "left","左边" };
	map<string, string> mymap1;
	mymap1.insert(pv1);
	mymap1.insert(pair<string,string>("right","右边"));
	mymap1.insert(make_pair("boy", "男孩"));
	//上面三种插入方式都是调用pair的构造来进行调用,区别就在于有名或者匿名而已
	mymap1.insert({"right","右边"});//pair支持多参数的隐式类型转换,这里走了pair构造的转换,该插入方式是最简单的
	
	//插入initializer list
	map<string, string> dict;
	dict.insert({ {"insert","插入"},{"sort","排序"},{"dog","狗"},{"girl","女孩"} });

	//插入一段迭代区间
	map<string, string> dict1;
	dict1.insert(dict.begin(), dict.end());

	//用迭代器遍历时要注意,因为pair没有对流输出进行重载,所以遍历时需要一个个打印它的成员变量
	map<string, string>::iterator it = dict.begin();
	while (it != dict.end())
	{
		//cout << (*it).first << ":" << (*it).second << endl;
		cout << it->first << ":" << it->second << endl;
		//上面这两种方式都可以

		++it;
	}
	cout << endl;
	//也可以用范围for进行遍历
	/*for(const auto& e : dict)
	{
		cout << e.first << ":" << e.second << endl;
	}
	cout << endl;*/
	//范围for的另一种写法,C++17及以上才支持,这种用法叫"结构化绑定"
	/*for (const auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;*/
	
	return 0;
}

输出结果:

  1. map的查

该接口是find函数,使用的方式与set完全类似,声明定义如下:

cpp 复制代码
iterator find(const key_type& k);
const_iterator find(const key_type& k) const;

与set的查找一样,都是通过关键字(key)来进行查找,不过map中的find返回的iterator,不仅可以确认要查找的key在不在,还可以找到key映射的value,同时通过返回的迭代器对value进行修改,但是关键字key还是一样不准修改

代码举例,经典的key/value场景,给一个单词,找出它的中文意思:

cpp 复制代码
#include<iostream>
#include<string>
#include<map>
using namespace std;

int main()
{
	map<string, string> dict = { {"left","左边"},{"right","右边"},
	{"auto","自动的"},{"sort","排序"},
	{"insert","插入"} };
	
	string str;
	while (cin >> str)
	{
		auto pos = dict.find(str);
		if (pos != dict.end())
		{
			cout << "->" << pos->second << endl;
		}
		else
		{
			cout << "单词不存在,请重新输入!" << endl;
		}
	}
	return 0;
}

输出结果:

3.map的删

与set完全类似,使用erase函数,也是通过传关键字或者迭代器的位置及迭代区间来进行单个/多个的删除,它的声明结构如下:

cpp 复制代码
iterator erase(const_iterator position);
size_type erase(const key_type& k);
iterator erase(const_iterator first, const_iterator last);

代码举例如下:

cpp 复制代码
int main()
{
	map<string, string> dict = { {"left","左边"},{"right","右边"},
	{"auto","自动的"},{"sort","排序"},
	{"insert","插入"},{"girl","女孩"},
	{"boy","男孩"}};
	for (const auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;
	cout << "删除第一个位置的关键字:" << endl;
	dict.erase(dict.begin());
	for (const auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;
	
	cout << "删除right:右边" << endl;
	dict.erase("right");
	for (const auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;

	cout << "全部删除:" << endl;
	dict.erase(dict.begin(), dict.end());
	for (const auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;
	return 0;
}

结果:

4.map的该

在查找时已经提到,find函数返回的迭代器是可以对value进行修改的,或者迭代器在遍历的过程中,也可以对value进行修改,代码如下:

cpp 复制代码
int main()
{
	map<string, string> dict = { {"left","左边"},{"right","右边"},
		{"auto","自动的"},{"sort","排序"},
		{"insert","插入"} };
	//对left进行
	auto ret = dict.find("left");
	if (ret != dict.end())
	{
		ret->second = "留下";
		cout << ret->first << ":" << ret->second << endl;
	}
	cout << endl;
	//用迭代器遍历进行修改
	for (auto& e : dict)
	{
		e.second += 'x';
		cout << e.first << ":" << e.second << endl;
	}
	return 0;;
}

5.map的operator[]

先来看该接口的声明结构:

cpp 复制代码
mapped_type& operator[](const key_type& k);

从声明结构中得到,给一个key,返回对应的value的引用,所以该接口可以充当一个查找+修改的功能

实际上,重载方括号的底层其实是用insert实现的,它的实现等价于调用这段代码:

cpp 复制代码
return (*this->insert(make_pair(k, mapped_type()))).second;

所以可以知道,重载的方括号还有插入的功能,上面这段代码有点复杂,涉及到了插入的返回值,我们可以再来重新看一遍插入函数的声明结构:

cpp 复制代码
pair<iterator, bool> insert(const value_type& val);

分析:上面的这个声明结构,涉及到了两个pair,第一个pair就是要传的参数,value_type,相当于pair<k,v>,第二个pair就是返回值,返回值的pair值中,第一个是迭代器,第二个是bool值,需要返回bool值是因为插入可能会成功,也可能会失败,毕竟map容器是不允许值冗余的。当插入成功时,bool值是true,返回的迭代器是插入成功的新的key结点的迭代器;当插入失败时,bool值是false,返回的迭代器是已经存在的key结点的迭代器。

总结:无论插入成功还是失败,返回pair<iterator,bool>对象的迭代器都会指向key所在的结点,那么也就意味着insert插入失败时充当查找的功能,正是这一点,insert可以用来实现operator[]

operator[]内部实现的代码如下:

cpp 复制代码
mapped_type& operator[](const key_type& val)
{
	pair<iterator, bool> ret = insert({ k,mapped_type() });
	iterator it = ret.first;
	return it->second;
}

代码解析:

  1. 若key不在map中,insert会插入key和mapped_type默认值,同时[]返回结点中存储mapped_type值的引用,那么我们可以通过引用来修改反映射值。所以[]具备插入+修改的功能
  2. 若key在map中,insert会插入失败,但insert返回pair对象的first是指向已存在的key结点的迭代器,同时[]会返回pair迭代器中存储mapped_type值的引用,所以[]具备了查找+修改的功能

[]的使用样例如下代码:

cpp 复制代码
int main()
{
	map<string, string> dict;
	dict.insert(make_pair("sort", "排序"));
	for (auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;
	cout << "key不存在->插入:" << endl;
	dict["left"]; //插入后value是string的默认值,所以是空
	for (auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;

	cout << "存在->修改:" << endl;
	dict["left"] = "留下";
	dict["left"]; //插入后value是string的默认值,所以是空
	for (auto& [k, v] : dict)
	{
		cout << k << ":" << v << endl;
	}
	cout << endl;
	
	cout << "查找:" << endl;
	cout << dict["left"] << endl;
	return 0;
}

输出结果:

6.multimap和map的差异

multimap和map的使用基本上是完全类似的,区别就在于multimap支持关键字key值冗余,那么它的insert/find/erase/count都会因为key冗余而有所差异,跟set和multiset完全一样,比如,find有多个key时,返回中序的第一个key,其次就是multimap不支持operator[],因为支持key冗余,operator[]就只能支持插入,不能支持值修改

相关推荐
TechNomad27 分钟前
二、Visual Studio2022配置OpenGL环境
c++·opengl
杨校1 小时前
杨校老师课堂之备战信息学奥赛算法背包DP练习题汇总
c++·算法·信息学竞赛·dp算法
居然是阿宋1 小时前
Java/Kotlin 开发者如何快速入门 C++
java·c++·kotlin
weixin_468466851 小时前
C++、C#、python调用OpenCV进行图像处理耗时对比
c++·图像处理·python·opencv·c#·机器视觉·opencvsharp
ChoSeitaku2 小时前
NO.24十六届蓝桥杯备战|二维数组八道练习|杨辉三角|矩阵(C++)
c++·线性代数·矩阵
_GR2 小时前
2017年蓝桥杯第八届C&C++大学B组真题及代码
c++·职场和发展·蓝桥杯
commonbelive2 小时前
c语言、c++怎么将string类型数据转成int,怎么将int转成string
c语言·c++
showmeyourcode0.o3 小时前
QT——对象树
c++·qt
Halsey Walker3 小时前
QT实现单个控制点在曲线上的贝塞尔曲线
c++·qt
daily_23333 小时前
c++领域展开第十五幕——STL(String类的模拟实现)超详细!!!!
android·开发语言·c++