【C++】set容器和map容器的基本使用

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

1、STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间一般没有紧密的关联关系,比如交换一下,它依旧是序列式容器,不会破坏逻辑结构。

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

3、map和set底层是红黑树,红黑树是一颗平衡⼆叉搜索树(时间复杂度为O(\log_{}N))。set是key搜索场景的结构,map是key/value搜索场景的结构。大家在学习set和map的时候,要结合我们上篇二叉搜索树中的两个场景的实现过程进行理解。

二、<set>

1、set容器

set是key搜索场景的结构,这里的第一个模板参数T其实就是K(key),第二个参数是仿函数,提供仿函数的目的是:保证set容器中的数据是按升序排列的还是降序排列的,第三个参数是空间配置器,set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数,基本上每个容器都有这个东西,我们暂且用它的缺省值即可。

一般情况下,我们都不需要传后两个模版参数。

set底层是用红黑树实现,增删查效率是O(\log N),它的迭代器遍历是走的搜索树的中序,由于二叉搜索树的性质,左子树小于根,根小于右子树,所以中序遍历的结果是有序的。

我们在此之前已经学习了vector/list等容器的使用,STL容器接口的设计高度相似,所以这里我们
就不再一个接口一个接口的介绍,只挑选比较重要的接口进行介绍。

(1)构造函数和迭代器

set的支持正向和反向迭代遍历,它的迭代器是双向迭代器,双向迭代器支持++/--,但是不支持+/-,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,set的iterator和const_iterator都不支持迭代器修改数据(修改key的值),如果修改就破坏了底层搜索树的结构。

//empty (1) 无参默认构造,第一个参数是仿函数对象
explicit set(const key_compare& comp = key_compare(),
             const allocator_type& alloc = allocator_type());

//range (2) 迭代器区间构造 
template <class InputIterator>
set(InputIterator first, InputIterator last,
    const key_compare& comp = key_compare(),
    const allocator_type& = allocator_type());
 
//copy (3) 拷⻉构造 
set(const set& x);

//initializer list (4) C++11支持: initializer 列表构造 
set(initializer_list<value_type> il,
    const key_compare& comp = key_compare(),
    const allocator_type& alloc = allocator_type());

//迭代器是一个双向迭代器 
iterator -> a bidirectional iterator to const value_type

//正向迭代器 
iterator begin();
iterator end();

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

对应代码说明:

int main()
{
    set<int> s1; //无参构造
    set<int>::iterator it1 = s1.begin();
    while (it1 != s1.end())
        cout << *it1 << " ", ++it1;
    cout << endl;

    cout << "-------------------" << endl;

    vector<int> v1({ 5,7,2,8,1,9,6,3,4 }); //vector支持用初始化列表构造
    set<int> s2(v1.begin(), v1.end()); //迭代器区间构造
    set<int>::iterator it2 = s2.begin();
    while (it2 != s2.end())
        cout << *it2 << " ", ++it2;
    cout << endl;

    cout << "-------------------" << endl;

    set<int> s3(s2); //拷贝构造
    set<int>::iterator it3 = s3.begin();
    while (it3 != s3.end())
        cout << *it3 << " ", ++it3;
    cout << endl;

    cout << "-------------------" << endl;

    set<int> s4({6,5,4,3,2,1}); //初始化列表构造
    cout << "正向迭代器:";
    set<int>::iterator it4 = s4.begin();
    while (it4 != s4.end())
        cout << *it4 << " ", ++it4;
    cout << endl;

    cout << "反向迭代器:";
    set<int>::reverse_iterator rit4 = s4.rbegin();
    while (rit4 != s4.rend())
        cout << *rit4 << " ", ++rit4;
    cout << endl;

	return 0;
}

运行结果:

不难发现,无论是以什么样形式初始化,它们的正向迭代器打印结果都是从小到大, 这是因为它的底层就是key搜索场景,迭代器在走的时候会进行中序遍历,在二叉搜索树中,中序遍历的结果就是顺序的,这里的第一个模板参数的仿函数默认是less,就是用less来控制插入时比较的方式,从而达到升序的效果,如果我们想让它是降序输出,那么只需显示传第一个模板参数的仿函数为greater,如果仿函数是greater那么底层二叉搜索树就是左孩子比父亲大,右孩子比父亲小,中序遍历时就是降序。如果T是自定义类型,若库中的仿函数不支持比较T的大小或者支持比较T的大小但比较的规则不是我们想要的那样,那我们就可以单独写一个仿函数,在默认构造时传我们单独写的仿函数对象给第一个默认构造参数。需要注意的是,第一个模板参数如果是排降序类型,如果要传第一个默认构造函数的参数,那也必须是这个排降序类型的对象;第一个模板参数如果是排升序类型,如果要传第一个默认构造函数的参数,那也必须是这个排升序类型的对象。

(2)增删查

set支持增删查,但不支持修改,因为修改可能会破坏内部性质。

相关接口如下:

//注:
//key_type -> The first template parameter (T) 即第一个模板参数T
//value_type -> The first template parameter (T) 即第一个模板参数T

//在set中key_type和value_type是一样的,都是第一个模板参数T(key)
//因为map底层是key/value场景,所以set中的key_type和value_type是为了与map保持一致
           
//1.单个数据插入,如果已经存在则插入失败 
pair<iterator, bool> insert(const value_type& val);

//2.列表插入,已经在容器中存在的值不会插入 
void insert(initializer_list<value_type> il);

//3.迭代器区间插入,已经在容器中存在的值不会插入 
template <class InputIterator>
void insert(InputIterator first, InputIterator last);

//1.删除一个迭代器位置的值,erase就是"有"就删,"没有"就不删,"没有"不会报错
iterator erase(const_iterator position);

//2.删除val,val不存在返回0,存在返回1
//这么做也是因为要兼容multiset,因为在multiset中val的值可能有多个
size_type erase(const value_type& val);

//3.删除一段迭代器区间的值 
iterator erase(const_iterator first, const_iterator last);

//查找val,返回val所在的迭代器,没有找到返回end(),它的查找逻辑就是按平衡二叉搜索树进行的,时间复杂度是O(logN)
iterator find(const value_type& val);

代码说明:

int main()
{
	//set容器在插入数据时:排序+去重
	//set<int> s1;  //默认排升序
	set<int, greater<int>> s1; //排降序
	s1.insert(5);
	s1.insert(2);
	s1.insert(7);
	s1.insert(5); //插入相同的值会失败

	s1.insert({ 2,8,3,9,2 }); //支持插入初始化列表,重复的值不会插入多次

	set<int>::iterator it = s1.begin();
	while (it != s1.end())
	{
		//*it = 1; //迭代器指向的内容不允许修改(set容器中的值不允许修改)
		cout << *it << " ",	++it;
	}
	cout << endl;

	cout << "------------------------------" << endl;

	set<string> strset = { "sort","insert","add" }; 
	//set容器中存放的是string类型的元素
	//string类型支持比较大小,它是按ASCII码进行比较的
	for (auto e : strset)
		cout << e << " ";
	cout << endl;

	cout << "------------------------------" << endl;
	
	set<int> s2 = { 4,2,7,2,8,5,9 };
	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	//删除最小值
	s2.erase(s2.begin()); //默认是升序且无重复值,begin()位置就是最小值
	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	//删除指定的值
	int x1;
	cin >> x1;
	int num = s2.erase(x1);
	if (num == 0)
		cout << x1 << "不存在!" << endl;
	else
		cout << x1 << "删除成功!" << endl;
	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	//下面的效果等同与上面
	int x2;
	cin >> x2;
	auto pos = s2.find(x2);
	if (pos != s2.end())
	{
		s2.erase(pos); //删除后,pos位置的迭代器就失效了
		//pos位置分为两种情况,第一:pos位置的结点只有左孩子或右孩子或是叶子节点,那么根据二叉搜索树的删除逻辑,删除后pos就是野指针了
		//第二:如果pos位置结点左右都有孩子,就用替换法删除,删除后,pos虽然不是野指针,但它指向的意义已经发生变化
		//这两种情况都可以称为迭代器失效,所以库中的erase返回值是一个迭代器,它返回删除位置的下一位置的迭代器
		//cout << *pos << endl; //因为迭代器已经失效,贸然访问,在vs2019下就会直接报错
		cout << x2 << "删除成功!" << endl;
	}
	else
		cout << x2 << "不存在!" << endl;
	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	cout << "------------------------------" << endl;

	int x3;
	cin >> x3;
	set<int> s3 = { 4,2,7,2,8,5,9 };
	//算法库中的查找
	auto pos1 = find(s3.begin(), s3.end(), x3);
	//set容器自身实现的查找
	auto pos2 = s3.find(x3);

	//算法库中的find是针对任何容器设计的,它的查找逻辑是在整个迭代区间遍历查找(暴力查找),所以它的时间复杂度是O(N)
	//set容器中的find是根据平衡二叉搜索树的查找逻辑进行查找的,所以它的时间复杂度是O(logN)
	//所以,在set容器中,尽量用它自身的find,不要用算法库中的find

	return 0;
}

运行结果:

(3)swap()

它的作用就是交换两个容器的根节点。

代码说明:

int main()
{
	set<int> s1({5,4,3,2,1});
	set<int> s2({ 10,9,8,7,6 });
	for (auto e : s1)
		cout << e << " ";
	cout << endl;

	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	cout << "---------------------------" << endl;

	s1.swap(s2);
	for (auto e : s1)
		cout << e << " ";
	cout << endl;

	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	return 0;
}

运行结果:

(4)clear()

它的功能就是删除容器中所有数据。

代码说明:

int main()
{
	set<int> s1({ 5,4,3,2,1 });
	for (auto e : s1)
		cout << e << " ";
	cout << endl;

	cout << "----------------------" << endl;

	s1.clear();
	for (auto e : s1)
		cout << e << " ";
	cout << endl;
	cout << "----------------------" << endl;

	return 0;
}

运行结果:

(5)count()

size_type是一个无符号整型,count的功能是查找val,返回val的个数,这里只有0和1两种情况,因为set容器中不允许有重复val,可以理解为:返回0代表该值在容器中不存在,如果为1则存在,这么做的原因还有一个就是要兼容multiset,因为在multiset中val的值可能有多个。

代码说明:

int main()
{
	set<int> s1({ 5,4,3,2,1 });
	for (auto e : s1)
		cout << e << " ";
	cout << endl;

	//不考虑删除的情况,用count看某值在不在set容器中是非常方便的
	int x;
	while (cin >> x)
	{
		if (s1.count(x))
			cout << x << "存在" << endl;
		else
			cout << x << "不存在" << endl;
	}

	return 0;
}

运行结果:

(6)lower_bound与upper_bound

lower_bound的功能是返回第一个大于等于val位置的迭代器(按照搜索树的规则找)

upper_bound的功能是返回第一个大于val位置的迭代器(按照搜索树的规则找)

一个是大于等于,一个是大于,注意不要混淆。

 这样设计方便我们去找一段区间,找到一段区间就可以针对这段区间进行相关操作。

代码说明:

int main()
{
	set<int> s1;
	for (int i = 1; i < 10; i++)
		s1.insert(i * 10); //10 20 30 40 50 60 70 80 90
	for (auto e : s1)
		cout << e << " ";
	cout << endl;

	//要求1:删除[30, 50]区间的值
	//要求2:删除[25, 55]区间的值

	//针对要求1:找到两个位置 >=30 和 >50
	auto itlow1 = s1.lower_bound(30); //返回第一个大于等于30位置的迭代器
	auto itup1 = s1.upper_bound(50); //返回第一个大于50位置的迭代器
	//这样就把set中在[30,50]这个区间的所有元素找出来了,删除即可
	s1.erase(itlow1, itup1); //删除的区间为"左闭右开"
	for (auto e : s1)
		cout << e << " ";
	cout << endl;

	cout << "----------------------------------------" << endl;

	set<int> s2;
	for (int i = 1; i < 10; i++)
		s2.insert(i * 10); //10 20 30 40 50 60 70 80 90
	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	//针对要求2:找到两个位置 >=25 和 >55
	auto itlow2 = s2.lower_bound(25); //返回第一个大于等于30位置的迭代器
	auto itup2 = s2.upper_bound(55); //返回第一个大于50位置的迭代器
	//这样就把set中在[25,55]这个区间的所有元素找出来了,删除即可
	s2.erase(itlow2, itup2); //删除的区间为"左闭右开"
	for (auto e : s2)
		cout << e << " ";
	cout << endl;

	return 0;
}

运行结果:

2、multiset容器

multiset和set的使用基本完全类似,主要区别点在于multiset支持值冗余,相同的值,插入到该结点的左边还是右边都可以,没有什么区别,但是要统一,不能一会插入到左边一会插入到右边,允许数据冗余那么insert/find/count/erase这几个函数就会与set有所差异。

代码说明:

int main()
{
	//multiset容器在插入数据时:排序+不去重
	multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };
	auto it = s.begin();
	while (it != s.end())
	{
		cout << *it << " "; //输出:2 2 4 4 4 4 5 7 8 9
		++it;
	}
	cout << endl;

	//相比set不同的是,x可能会存在多个,find查找中序的第一个
	//如何确定x是中序第一个呢?
	//因为中序遍历遵循:左子树 根 右子树
	//所以如果找到x,就不需要到右子树找了,因为右子树如果有x不可能是第一个
	//所以如果找到x,再继续从它的左子树中找,如果没有找到,那么当前位置就是中序第一个x,如果找到,那么继续从它的左子树找,直到找不到,最后一个x就是中序第一个x,最多找高度次,所以它的时间复杂度也是:O(logN)
	//为什么要返回中序第一个呢?
	//因为找到中序第一个,迭代器就能不断++找到所有的x,所以库里面要求返回中序第一个
	int x;
	cin >> x;

	//输出所有的x
	auto pos = s.find(x);
	while (pos != s.end() && *pos == x)
	{
		cout << *pos << " ";
		++pos;
	}
	cout << endl;

	//删除所有的x
	//pos = s.find(x);
	//while (pos != s.end() && *pos == x)
	//{
		//pos = s.erase(pos); //返回删除位置的下一位置的迭代器
	//}
	//cout << endl;

	//erase不仅可以传迭代器,也可以传具体的某个值
	//上面删除所有的x的方法有点麻烦,我们也可以这样:
	s.erase(x); //删除multiset容器中所有的x
	for (auto e : s)
		cout << e << " ";
	cout << endl;

	//count会返回x的实际个数 
	cout << s.count(x) << endl;
	
	return 0;
}

运行结果: 

三、<map>

1、map容器

map是key/value搜索场景的结构,这里的前两个模板参数分别是key和value,第三个模板参数是仿函数,提供仿函数的目的是:保证map容器中的数据是按升序排列的还是降序排列的(这里的排升序是根据key的值排的),第四个参数是空间配置器。一般情况下,我们都不需要传后两个模版参数。

map底层是用红黑树实现,增删查改效率是O(\log N) ,迭代器遍历是走的中序,所以是按Key有序顺序遍历的。

(1)pair类型

map底层的红黑树结点中的数据,使用pair<const Key,T>存储键值对数据。

下面是库中pair的一部分源码:

typedef pair<const Key, T> value_type;

template <class T1, class T2>
struct pair
{
	typedef T1 first_type;
	typedef T2 second_type;
	T1 first;
	T2 second;

	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)
	{}
};

template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y) //创建pair对象
{
	return (pair<T1, T2>(x, y));
}

pair的第一个模板参数就是key,第二个模板参数就是value,对应的就是first就是key,second就是value。相当于将key和value捆绑起来放在名为pair的结构体中,这个结构体有两个成员first和second,first就是key,second就是value。因为key不能被改变所以加了const,value可以被允许改变所以没有加const。将它们两个捆绑起来目的是在后面它们迭代器解引用时方便打印数据。

value_type就是pair类型。

(2)构造函数和迭代器

map容器支持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,map支持修改value数据,但是不支持修改key数据,修改关键字数据,会破坏底层搜索树的结构。

map的构造我们关注以下几个接口即可:

//empty (1) 无参默认构造 
explicit map(const key_compare& comp = key_compare(),
	         const allocator_type& alloc = allocator_type());

//range (2) 迭代器区间构造 
template <class InputIterator>
map(InputIterator first, InputIterator last,
	const key_compare& comp = key_compare(),
	const allocator_type & = allocator_type());

//copy (3) 拷贝构造 
map(const map& x);

//initializer list (4) initializer 列表构造 
map(initializer_list<value_type> il,
	const key_compare& comp = key_compare(),
	const allocator_type& alloc = allocator_type());

//迭代器是一个双向迭代器 
iterator->a bidirectional iterator to const value_type

//正向迭代器 
iterator begin();
iterator end();

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

它跟set容器用法几乎一样,这里只讲一个初始化列表构造:

#include <map>
int main()
{
	pair<string, string> kv1("first", "第一个");
	pair<string, string> kv2("second", "第二个");
	pair<string, string> kv3("third", "第三个");

    //用初始化列表(3个有名对象)进行初始化
	map<string, string> dict1({ kv1,kv2,kv3 });

    //用初始化列表(3个匿名对象)进行初始化(前提是pair的构造函数支持二参构造)
	map<string, string> dict2({ pair<string,string>("one","一"),pair<string,string>("two","二"),pair<string,string>("three","三") });

    //用初始化列表(3个临时对象)进行初始化,隐式类型转换
	map<string, string> dict3({ {"action","行动"},{"plan","计划"},{"success","成功"} });

	return 0;
}

调试结果:

(3)增删查

map插入的是pair键值对数据,跟set所有不同,但是查和删的接口只用关键字key跟set是完全类似的,不过find返回iterator,不仅仅可以确认key在不在,还找到key映射的value,同时通过迭代还可以修改value。

map的增删查关注以下几个接口即可:

//key_type    -> The first template parameter (Key) 即第一个模板参数Key
//mapped_type -> The second template parameter (T)  即第二个模板参数T(value)
//value_type  -> pair<const key_type,mapped_type>   即pair<const Key,T> (pair<const key,value>)

//单个数据插入,如果已经存在key则插入失败,若存在key相等但value不相等也会插入失败 
pair<iterator, bool> insert(const value_type& val);

//列表插入,已经在容器中存在的值不会插入
void insert(initializer_list<value_type> il);

//迭代器区间插入,已经在容器中存在的值不会插入
template <class InputIterator>
void insert(InputIterator first, InputIterator last);

//查找k,返回k所在的迭代器,没有找到返回end() 
iterator find(const key_type& k);

//删除一个迭代器位置的值 
iterator erase(const_iterator position);

//删除k,k存在返回0,存在返回1 
size_type erase(const key_type& k);

//删除一段迭代器区间的值
iterator erase(const_iterator first, const_iterator last);

代码说明:

int main()
{
	map<string, string> dict1;
	pair<string, string> kv1("first","第一个");
	dict1.insert(kv1); //直接传一个有名对象
	dict1.insert(pair<string,string>("second", "第二个")); //传匿名对象
	dict1.insert(make_pair("third","第三个"));//make_paia是一个模板,它接收两个模板参数,并返回一个pair对象,它可以减少代码长度
	dict1.insert({ "sort","排序" });//C++11,隐式类型转换,{"sort","排序"}可以转换成一个pair的对象(前提是pair支持两个参数的构造函数)
	
	//插入失败
	dict1.insert({ "sort","排序xxx" }); //在map中,如果插入和key的值相等但与value的值不相等,它是不会更新value的,因为插入的时候只看key,key如果相等,就不插入

	//遍历dict
	//map<string, string>::iterator it1 = dict.begin();
	auto it1 = dict1.begin();
	while (it1 != dict1.end())
	{
		//cout << *it1 << endl; //*it1解引用就是pair类型,pair不支持流插入和流提取,所以不能直接这样写

		//可以这样写:
		//cout << (*it1).first << ":" << (*it1).second << endl;

		//it1->first += 'x'; //key不支持修改
		it1->second += 'x'; //value支持修改

		//也可以这样写:
		cout << it1->first << ":" << it1->second << endl;
		//本质上是这样的:
		//cout << it1.operator->()->first << ":" << it1.operator->()->second << endl;

		++it1;
	}
	cout << endl;

	cout << "---------------------------" << endl;

	//可以不一个个插入,直接插入初始化列表会更方便
	map<string, string> dict2;
	dict2.insert({ {"left","左边"},{"right","右边"},{"up","向上"}});//隐式类型转换

	auto it2 = dict2.begin();
	while (it2 != dict2.end())
	{
		cout << it2->first << ":" << it2->second << endl;
		++it2;
	}
	cout << endl;
	return 0;
}

因为map的删除和查找只和key有关,所以它的删除和查找几乎和set一样,大家可以直接参考set容器的删除和查找。 

(4)其它

map的swap、clear、count、 lower_bound与upper_bound几乎和set一模一样,直接参考set容器的这一部分即可。

(5)equal_range

它的功能是返回k的左闭右开区间,如果k为20,就返回[20,第一个大于b的值),这一迭代区间,这一函数在multimap中效果很好,因为它可以找到所有的k。

(6)operator[]

在map容器中,它重载了[],这点是与set容器是不同的。

它虽然重载了[],但此时的意义已经和数组中那个下标引用操作符不同了,这里是传一个key,返回对应的value的引用。

这里的[]具有三重功能:插入、查找、修改。这里的插入指的是如果传入的key,在map中找不到,那就将key插入到map容器中,它的底层会调用insert。查找是指要先查找key是否存在这个过程。修改是指我们可以对返回值value进行修改。

insert函数也就是下面这个:

pair<iterator, bool> insert(const value_type& val);

我们在上面讲了它的用法,但没讲它的返回值,接下来我们就研究一下它的返回值:

它的返回值是pair类型,但是它与上面pair是不同的,模板参数就不一样,⼀个是map底层红⿊树节点中存的pair<const key, value>,另⼀个是insert返回值pair<iterator,bool>。

这里的pair中第一个模板参数是一个迭代器,如果插入成功,返回新插入的元素位置的迭代器;如果插入失败,返回key位置的迭代器。也就是说无论插入成功还是失败,返回pair<iterator,bool>对象的first都会指向key所在的迭代器。

这里的pair中第二个模板参数bool类型就是当key存在,那就插入失败,返回false;当key不存在,那就插入,返回true。

插入成功:pair<新插入值所在的迭代器,true>

插入失败:pair<已经存在的和key相等的值所在的迭代器,false>

insert插入失败时充当了查找的功能,正是因为这⼀点,insert可以用来实现operator[]。

operator的内部实现就像下面这样:

mapped_type& operator[] (const key_type& k)
{
	//1.如果key不在map中,insert会插入key和mapped_type(value)的默认值
	//同时[]返回结点中存储的mapped_type(value)值的引用,那么我们可以通过引用修改value值。所以[]具备了插入+修改功能
	
	//2.如果k在map中,insert会插入失败,但是insert返回pair对象的first是指向与key值相同的结点的
	//迭代器,同时[]返回结点中存储mapped_type(value)值的引用,所以[]具备了查找+修改的功能

	pair<iterator, bool> ret = insert({ k, mapped_type() });
	iterator it = ret.first;

	return it->second;
}

我们来举个例子,可能会帮助大家更好的理解:

int main()
{
	//利用find和iterator修改功能,统计水果出现的次数 
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	map<string, int> countMap;
	for (const auto& str : arr)
	{
		//先查找水果在不在map中 
		//1、不在,说明水果第一次出现,则插入{水果, 1} 
		//2、在,则查找到的节点中水果对应的次数++ 
		auto ret = countMap.find(str);
		if (ret == countMap.end())
			countMap.insert({ str, 1 });
		else
			ret->second++;
	}

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

	return 0;
}

这段代码是统计各个水果出现的次数的,我们在上一篇二叉搜索树中讲到过,只不过这里换成了map容器进行操作,底层还是一样的。

运行结果:

我们上面学习了解了[],现可以对这段代码进行优化:

int main()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	map<string, int> countMap;
	for (const auto& str : arr)
	{
		countMap[str]++; //一句代码搞定问题
		//如果map中没有str,那么就插入str,并初始化value为0,返回value的引用,接着++,相当于首次插入str,并将对应的value置为1
		//如果map中有str,那么就返回当前str位置上的value的引用,接着++,相当于,只对value进行了加1操作,value就是水果的个数,逻辑上没有任何问题
	}

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

	return 0;
}

运行结果: 

结果没有问题,大大简化了代码的复杂度。 

我们再来看一个例子:

int main()
{
	map<string, string> dict;

	dict.insert(make_pair("sort", "排序"));

	//insert不存在:插入  
	dict["insert"]; //insert({"insert", string()})

	//left不存在:插入+修改
	dict["left"] = "左边"; //将""修改为"左边"

	//left存在:修改
	dict["left"] = "左边、剩余"; //将"左边"修改为"左边、剩余"

	//left存在:查找(确认left在才能这么用,否则就是插入了)
	cout << dict["left"] << endl;

	//up不存在:插入
	cout << dict["up"] << endl; //输出:""

	return 0;
}

2、multimap容器

multimap和map的使用基本完全类似,主要区别点在于multimap支持关键值key冗余,那么multimap在插入数据时一定会成功,除非空间不够,否则就不会插入失败,insert/find/count/erase都围绕着支持关键值key冗余有所差异,这里跟set和multiset完全一样,比如find时,有多个key,返回中序第一个;erase时,它的内部先利用find找到第一个key,然后开始依次删除,直到key删除完全,其次就是multimap不支持[],因为multimap支持key冗余,调用[]会返回对应的value,如果支持key冗余,那调用后返回哪一个value呢?所以multimap不支持[]。

代码说明:

int main()
{
	multimap<string, string> dict;

	//插入一定成功
	dict.insert({ "sort", "排序1" });
	dict.insert({ "sort", "排序1" });
	dict.insert({ "sort", "排序2" });
	dict.insert({ "sort", "排序3" });
	dict.insert({ "sort", "排序3" });
	dict.insert({ "string", "字符串" });

	//将sort全部删除
	dict.erase("sort");

	return 0;
}

erase执行前调试结果:

erase执行后调试结果: 

四、结语

本篇内容到这里就结束了,主要讲了set和multiset容器以及map和multimap容器的基本使用,希望对大家有帮助,祝生活愉快,我们下一篇再见! 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第L2周:机器学习|线性回归模型 LinearRegression:2. 多元线性回归模型

本文为365天深度学习训练营 中的学习记录博客原作者&#xff1a;K同学啊 任务&#xff1a; ●1. 学习本文的多元线形回归模型。 ●2. 参考文本预测花瓣宽度的方法&#xff0c;选用其他三个变量来预测花瓣长度。 一、多元线性回归 简单线性回归&#xff1a;影响 Y 的因素唯一&…

1、Spring Boot 3.x 集成 Eureka Server/Client

一、前言 基于 Spring Boot 3.x 版本开发&#xff0c;因为 Spring Boot 3.x 暂时没有正式发布&#xff0c;所以很少有 Spring Boot 3.x 开发的项目&#xff0c;自己也很想了踩踩坑&#xff0c;看看 Spring Boot 3.x 与 2.x 有什么区别。自己与记录一下在 Spring Boot 3.x 过程…

Linux ssh 免密登录配置

参考资料 ~/.ssh/configについて~/.ssh/configを使ってSSH接続を楽にする.ssh/configファイルでSSH接続を管理する 目录 一. 密钥生成1.1 生成工具1.1.1 OpenSSH1.1.2 Git 1.2 生成命令1.3 注意事项1.4 解决路径中的用户名乱码 二. 将公钥配置到目标服务&#xff0c;免密登录2…

如何在 Windows 10 上恢复未保存/删除的 Word 文档

您是否整夜都在处理重要的 word 文件&#xff0c;但忘记保存它&#xff1f;这篇文章是给你的。在这里&#xff0c;我们将解释如何恢复未保存的 word 文档。除此之外&#xff0c;您还将学习如何恢复已删除的 word 文档。 从专业人士到高中生&#xff0c;每个人都了解丢失重要 W…

从HarmonyOS Next导出手机照片

1&#xff09;打开DevEco Studio开发工具 2&#xff09;插入USB数据线&#xff0c;连接手机 3&#xff09;在DevEco Studio开发工具&#xff0c;通过View -> Tool Windows -> Device File Browser打开管理工具 4&#xff09;选择storage -> cloud -> 100->fi…

JDBC 概述

JDBC 概述 JDBC的基本概念与功能JDBC的工作原理JDBC的组件与类JDBC的类型与特性JDBC的应用场景 JDBC&#xff08;Java Database Connectivity&#xff09;即Java数据库连接&#xff0c;是Java编程语言用于与数据库进行连接和操作的API&#xff08;应用程序编程接口&#xff09;…

Linux:深入理解冯诺依曼结构与操作系统

目录 1. 冯诺依曼体系结构 1.1 结构分析 1.2 存储结构分布图 2. 操作系统 2.1 概念 2.2 如何管理 2.3 什么是系统调用和库函数 1. 冯诺依曼体系结构 1.1 结构分析 不管是何种计算机&#xff0c;如个人笔记本电脑&#xff0c;服务器&#xff0c;都是遵循冯诺依曼结构。…

注册安全分析报告:科研诚信查询平台无验证方式导致安全隐患

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

【Linux】进程管理:状态与优先级调度的深度分析

✨ 山海自有归期&#xff0c;风雨自有相逢 &#x1f30f; &#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;Linux—登神长阶 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1…

SQL:函数以及约束

目录 介绍 函数 字符串函数 数值函数 日期函数 流程函数 约束 总结 介绍 说到函数我们都不陌生,在C,C,java等语言中都有库函数,我们在平时也是经常使用,函数就是一段代码,我们既可以自定义实现,又可以使用库里内置的函数;从来更加简洁方便的完成业务;同样的在SQL中也有…

从0到1深入浅出构建Nest.Js项目

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用JavaScript 的渐进增强的能力&#xff0c;使用并完全支持 TypeScript &#xff08;仍然允许开发者使用纯 JavaScript 进行开发&#xff09;&#xff0c;并结合了 OOP &#xff08;面向对…

Spring Boot技术栈:打造高效在线商城

2 相关技术 2.1 Springboot框架介绍 Spring Boot是由Pivotal团队提供的全新框架&#xff0c;其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置&#xff0c;从而使开发人员不再需要定义样板化的配置。通过这种方式&#xff0c;Spring…

sentinel原理源码分析系列(三)-启动和初始化

本文是sentinel原理源码分析系列第三篇&#xff0c;分析sentinel启动和初始化 启动/初始化 sentinel初始化分两块&#xff0c;静态初始和适配器(包括aop) 静态初始 1. Root EntranceNode 如果我们用一栋楼类比资源调用&#xff0c;root EntranceNode好比一栋楼的大门&…

vulnhub-unknowndevice64 2靶机

vulnhub&#xff1a;https://www.vulnhub.com/entry/unknowndevice64-2,297/ 导入靶机&#xff0c;放在kali同网段&#xff0c;扫描 靶机在192.168.81.9&#xff0c;扫描端口 啥啊这都是&#xff0c;详细扫描一下 5555是adb&#xff0c;6465是ssh&#xff0c;12345看样子应该是…

简单的springboot 编写Socket服务接口

简单的springboot 编写Socket服务接口 1.需求 我们项目中有部分老接口为票据接口&#xff0c;其中实现为java socket形式进行实现&#xff0c;但是其中大部分信息都是原始公司封装的包进行实现的&#xff0c;想要修改非常费劲&#xff0c;所以此处简单了解了一下socket&#…

洛谷 P11045 [蓝桥杯 2024 省 Java B] 最优分组

[Problem Discription] \color{blue}{\texttt{[Problem Discription]}} [Problem Discription] [Analysis] \color{blue}{\texttt{[Analysis]}} [Analysis] 首先得注意这么一点&#xff1a; k k k 必须得是 n n n 的因数&#xff08;这里的 n , k n,k n,k 对应于题目的 N ,…

量化交易backtrader实践(三)_指标与策略篇(2)_内置指标A开头

在第1节中&#xff0c;我们学习了移动平均线的原理&#xff0c;中位数以及正态分布的概念&#xff0c;并通过python手工做了一个双均线的策略回测。了解了怎么用pandas计算移动平均线&#xff08;rollingmean)&#xff0c;怎么得到某一列上1个的值&#xff08;shift)&#xff0…

【算法与图】通向高效解决方案的钥匙

文章目录 遍历算法BFS&#xff08;广度优先遍历&#xff09;1. 什么是 BFS&#xff1f;2. 特点和应用3. BFS 示例 DFS&#xff08;深度优先搜索&#xff09;1. 什么是 DFS&#xff1f;2. DFS 的基本步骤3. 特点4. DFS 的应用5. DFS 示例 最小生成树问题1. 什么是最小生成树&…

【mmengine】优化器封装(OptimWrapper)(入门)优化器封装 vs 优化器

MMEngine 实现了优化器封装&#xff0c;为用户提供了统一的优化器访问接口。优化器封装支持不同的训练策略&#xff0c;包括混合精度训练、梯度累加和梯度截断。用户可以根据需求选择合适的训练策略。优化器封装还定义了一套标准的参数更新流程&#xff0c;用户可以基于这一套流…

虚拟机三种网络模式详解

在电脑里开一台虚拟机&#xff0c;是再常见不过的操作了。无论是用虚拟机玩只有旧版本系统能运行的游戏&#xff0c;还是用来学习Linux、跑跑应用程序都是很好的。而这其中&#xff0c;虚拟机网络是绝对绕不过去的。本篇文章通俗易懂的介绍了常见的虚拟网络提供的三种网络链接模…