set/map学习

news2024/11/19 4:31:09

我们要开始学习map和set的使用,虽然使用更加复杂,但是STL整体的设计,本身就具有很强的前瞻性和延续性,比如说迭代器等,我们顺着文档来看。这也是除了vector之外最重要的容器,当然还有unordered_map 和 unord_set,其实这几个也是C++里面用的最多的。

Set学习

模版参数

这里的set呢,它只有一个模版参数可就是T,然后还有一个比较器,默认是小于,它给一个比较器呢,就是为了,有些地方默认的operator <重载的比较不符合你的要求,假设你用的是指针,但是指针是按地址比较大小,当这个T是一个指针的时候,但是你不想按地址比较大小,那么原生的行为不符合怎么办,那你是不是就可以传仿函数呀,去控制里面的比较规则呀。在下面就是空间配置器,它是负责申请内存。
在这里插入图片描述

insert()

在这里插入图片描述
我们看到函数参数中有一个,value_type类型,但是不知道这个是什么类型。我们先写一个最简单来看看。
》set s;set 没有push() ,线性表、链表、队列等都是线性结构,才会有push()接口 。后面非线性结构的都是insert()接口了。我们随便插入一些数据s.insert(4);s.insert(5)…我们插入5个值。然后我们定义一个迭代器,set::iterator it = s.begin();这个set呢,它是一个双向迭代器,它底层用了特殊的算法迭代,是走的另一种非递归,后面也会讲,不用像我们在前面的二叉树OJ那里用栈模拟,但是它这里非递归有前提的,前提就是,它要有三叉链,它的逻辑底层是有parent成员变量的,反正我们先不管,后面我们才讨论底层。
》然后我们while循环遍历,while(it != s.end()),cout<< *it << " ";it++然后我们看到,走出来就是有序的!其实它不仅仅是有序的,它是在有序的情况下还加了一些东西,如果值已经有了,它还会不会插入?不会了!所以严格来说,它不是排序,而是一个排序➕去重!
》它这块支持迭代器,是不是有普通迭代器支持可读可写,还有const迭代器只读。然后它还有反向迭代器,就不演示了。
》它既然支持迭代器,那么就会支持范围for。其实范围for和我们迭代器在底层上是一样的,只是在上层看来有些区别。
》我们发现,const迭代器和普通迭代器都是不支持修改的 ,虽然一个叫iteratro,一个叫const iterator,但是底层都是用的一个const_iterator来实现的。
》另外重载的两个insert()函数用的很少。平时也很少用在某一个位置插入,这个接口价值很小。
》insert()不会有迭代器失效的问题,不像顺序表,挪动数据会影响相对关系,它的erase()是会有迭代器失效的问题。

void test_set1()
{
	set<int> s;
	s.insert(4);
	s.insert(5);
	s.insert(2);
	s.insert(1);
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(1);


	// 排序 + 去重 
	set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		//*it = 10;
		cout << *it << " ";
		++it;
	}
	cout << endl;

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

empty()/size()/max_size()

erase()/find()—两个是要配合的

在这里插入图片描述
在这里插入图片描述

erase()函数主要有两种,一种是在某个位置删除,另外一种是删除某一个具体的值。第三种删除特定的区间,这个用的很少。
》这块要玩删除的话,就得配合find()函数。我们前面插入了很多值,我们随便set::iterator it = find(2)一个值,那么我们**怎么判断是找到还是没找到呢?**如果元素被找到了,是会返回value值对应的迭代器,查找失败就会返回end()迭代器。是不是就是if(pos != s.end())就是找到了呀。
》我们来对比一下,set自带的find()和算法里面的find()有什么区别呢? set::iterator it = find(20) 和 pos = find(s.begin(),s,end(),20),算法里面的find,能不能行呢?是行的。它们的区别很大,数据量小还好,数据量大就会差别很大。set的find()的效率是logN,而算法的查找效率是O(N),因为算法的是暴力查找。这个算法的find()查找不是给set用的,而是给像vector它们用的。

void test_set2()
{
	set<int> s;
	s.insert(4);
	s.insert(5);
	s.insert(2);
	s.insert(1);
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(1);

	set<int>::iterator pos = s.find(20);  // O(logN)
	if (pos != s.end())
	{
		cout << "set.find找到了" << endl;
	}

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

》如果我们要erase了呢?,我想将我从键盘中输入的值erase()掉,通过find()查找我们输入的值再去erase(迭代器)。当erase()成功之后,对应的位置就不能访问了,已经是失效了。我们可以通过查找进行持续的删除。我们还可以直接传参数去删除,即erase(3);也就是你可以理解成,这个erase(3),相当于调用了find()➕erase(迭代器)。
》前面第一种是先是找到了再去删除,它们会有什么区别呢?第二种用法,如果没有找到直接去删除是不会报错的;而你第一种调用了find()没有找到,你还是去删除,是会报错的。
》size_type erase(value)是会返回一个size_type类型的数据,他为什么不用bool类型呢,是因为它和另外讲的东西契合起来。我们能够看到,删除成功是返回1,删除失败是会返回0,为什么是返回这个,而不是返回bool呢?因为待会儿我们还要学另外一个版本,另外一个版本是允许冗余的。

void test_set3()
{
	set<int> s;
	s.insert(4);
	s.insert(5);
	s.insert(2);
	s.insert(1);
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(1);

	cout << s.erase(3) << endl;
	cout << s.erase(30) << endl;

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

	set<int>::iterator pos = s.find(3);
	if (pos != s.end())
		s.erase(pos);

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

	/*int x;
	while (cin >> x)
	{
		set<int>::iterator pos = s.find(x);
		if (pos != s.end())
		{
			s.erase(pos);
			cout << "删除" << x << "成功" << endl;
		}
		else
		{
			cout << x << "不在set中" << endl;
		}

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

	if (s.count(5))
	{
		cout << "5在" << endl;
	}

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

swap()

swap()就跟我们以前讲的一样,比如说你要交换两个set的对象,用算法的swap()直接交换是深拷贝,是树的拷贝,代价很大,所以set自己有一个swap(),它们交换指针,代价小很多。

count()

在这里插入图片描述

count()函数这个设计是和一个版本对应起来的,就是你给我一个值,我看一下这个值在我set里面有几个,在这里的set而言,无非就是1个和0个。但是待会儿还有一个对称的版本。我们说了,有些场景可能需要的不是排序➕去重,而只是排序,那么它们就设计了multile版本,它是多样性的意思,它是允许冗余的,它们的接口设计都是保持的一致。
》count()函数有一个非常有意义的点就是,判断在不在,比用find()方便。比如说,我要判断一个值在还是不在,用count()是不是就很方便呀,比如说我要count(3),if(s.count(3))看一下这个3在不在,在的话是会返回1,不在的话,是会返回0。如果用find()的话就没那么方便了吗,要这样if(s.find(3) != s.end())是不是相对来说不太方便了。

lower_bound()函数

在这里插入图片描述
lower_bound()是干嘛呢?比如lower_bound(30)是帮你去找 >= 30的值的迭代器,但是它在这儿又有一点问题,lower_bound()和upper_bound()的返回值还有点不一样。我们试验一下。
》你看set::iterator lowIt = s.lower_bound(3);如果有3,它会返回3位置的迭代器,没有就返回 > 3的值,我们插入的值里面是有3的,所以返回的是3 的迭代器。如果是set::iterator lowIt = s.lower_bound(6);我们插入里面没有6,那它会返回什么呢?返回的是7的迭代器。
》它什么情况下比较有用么,比如说:要求删除 >= x的所有值,那么你就不用遍历去删除了,这里的lower_bound()函数就可以帮干这件事情,怎么删除呢?不要忘了,我们的erase()是支持迭代器区间的删除!那么erase()左区间可以给个lowIt变量,右区间可以给end(),相当于是左闭右开【),即erase(lowIt,s.end());

void test_set4()
{
	set<int> s;
	s.insert(4);
	s.insert(5);
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(7);
	s.insert(9);

	// 返回>= val得位置迭代器  3返回3位置  6 返回7位置
	/*set<int>::iterator lowIt = s.lower_bound(3); 存在
	lowIt = s.lower_bound(6); 不存在*/
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	// 要求删除>=x的所有值
	int x;
	cin >> x;
	set<int>::iterator lowIt = s.lower_bound(x);
	s.erase(lowIt, s.end());

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

upper_bound()

在这里插入图片描述
比如upper_bound(60),如果有60的话,它是不回返回60,而是返回 > 60的值,我们来验证一下。这里我们比如说是,set::iterator upIt = s.upper_bound(5),我们传一个5,然后再来一个,set::iterator upIt = s.upper_bound(6),我们传的是6,这两个区别就是一个值在我们插入的里面,一个是不存在。我们来看看结果,我传的是5,有5它返回5了吗?答案是:不会的!它返回的是7,我传的是6,它返回的是7。所以,这里的upper_bound(),它要找的不是 >=的值,它是返回 > x位置的迭代器
》所以upper_bound和lower_bound,一个是返回 >=,一个是返回 >。其实他们**两个可以配合起来并配合ease(),贯彻“左闭右开【)”。**比如说要删除 x <= [] <=y的区间,是不是就可以将这两个配合起来使用了呀。比如auto leftIt = s.lower_bound(x),auto rightIt = s.upper_bound(y),erase(letfIt,rightIt)所以它会删除【x,y】

void test_set5()
{
	set<int> s;
	s.insert(4);
	s.insert(5);
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(7);
	s.insert(9);

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

	// 返回>x位置的迭代器  -》 都是返回 7位置的迭代器
	//set<int>::iterator upIt = s.upper_bound(5);  // 存在
	//upIt = s.upper_bound(6); // 不存在

	// 删除x <=  <= y的区间 删除 [x,y]
	int x, y;
	cin >> x >> y;
	auto leftIt = s.lower_bound(x);  // [
	auto rightIt = s.upper_bound(y); // )
	s.erase(leftIt, rightIt);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
}

讲讲multiset版本

multiset它是允许键值冗余的,它的用法和set一样的,但就是要记一下它的名字,比如mutilset mS;它是排序不去重
》真正有区别的就是count()函数,比如说你要统计一个值s.count(1);set版本那边的count没有给bool类型的返回值,而是给int类型返回值,这其实是为保证接口的设计一致。其实set不需要count,是个bool就行了,但是为了和multiset保持一致,返回值类型也是int。
》同样的,set版本里面的erase()函数的返回值不是bool,而是size_type,也是为了和multiset里面的erase()设计保持一致,可以返回删除了几个对应的值。
》还有一个区别就是这里的find()函数,比如我这里有多个同样的值,那么我auto pos = s.find(3)会返回哪个3的迭代器呢?那么这里就可以做一件事情,它会返回中序的第一个,可以由while(pos != s.end())cout << *pos << " ";++pos验证。我们以后讲了搜索树,会知道,插入的时候,树是会旋转的,所以同样的值插入在左边还是右边都不影响。

void test_set6()
{
	multiset<int> s;
	s.insert(4);
	s.insert(5);
	s.insert(2);
	s.insert(1);
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(1);
	s.insert(3);
	s.insert(3);
	s.insert(3);
	s.insert(3);

	// 排序 
	set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		//*it = 10;
		cout << *it << " ";
		++it;
	}
	cout << endl;

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

	cout << s.count(1) << endl;
	cout << s.erase(1) << endl;
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	// 多个x的话,find返回中序第一个x
	auto pos = s.find(3);
	while (pos != s.end())
	{
		cout << *pos << " ";
		++pos;
	}
	cout << endl;
}

看一下题目

题一:

两个数组的交集,找交集怎么找呢?是不是可以考虑这样,你也有,我也有的就是交集。
》最简单的方案就是,将其中一个数组放进set里面,我直接去你里面找,是不是太慢了。我们可以遍历将值inser()插入进set里面。然后继续遍历另一个数组,在我们的set里面找对应的值,那其中是不是就可以用count()函数而不必调用find()函数呀,然后,存在的话,就将值插入到我们另一个开辟的数组里面。但是这里有可能过不去,为什么呢?因为在我们开辟的vector来存交集的值,会有重复!因为交集是隐形的要求你去重的。
》那你的另一个数组是不是也用set来去重一下呀。但是这个算法不够高效,因为,假设你set里面有n个元素,那么每调用一次count()的时间复杂度是logN,那么总的时间复杂度是不是N*logN。
》还有一个优化算法,这个优化算法在很多找交集都能有效。找并集的话,只需要一个set就行了。现在来说一个交集/差集的优化算法,这个算法不一定要set,但是set用在这里刚刚好。
》我们inset()到set之后是不是就相当于有序了呀,有了排序就好说。如果你要求并集的话,只要放到一个set里面就行了,它是可以去重的。那么交集和差集怎么搞呢,有些地方需要同步,同步数据的话就要找交和差集。
》两个数组已经是有序了的,然后分别有一个指针先是指向各自的开始。交集的话:两个相比较的话,1.小的++;2.相等的就是交集,同时++;3.有一个结束就结束了。这个优化之后就变成了时间复杂度是2N了。
》这个算法除了可以找交集,还可以找差集。1.相比较,小的就是差集,小的++;2.相等的话,同时++;3.一个走到头了,另一个剩下的是不是也属于差集了。
》差集和交集在,服务器和你网盘需要同步的时候是有用到的。比如你有一个服务器文件夹和一个本地文件夹,你要将本地文件同步到服务器文件里面,相当于备份嘛,那是不是一方删除,另一方也会删除,你添加的,另一方是不是也要添加呀。
在这里插入图片描述
在这里插入图片描述

map

首先map数据结构里面呢,有一个Key和Value。它里面有一个valuse_type类型,它里面不是把数据分开存储的,不像我们前面讲的,自己写的搜索树里面,单独分开定一个key和value,而map,它是将key和value是放在一个结构里面的,这个结构是在C++里面被定义成pair<>类型,pair就是一对一对的意思,在map这里呢叫做键值对。

insert()

在这里插入图片描述
》我们先用map来存一个字典的例子,map的insert()插入的一个值是value_type类型的,value_type其实就是pair<>类型的数据。pair<>类型呢,有两个参数,第一个参数叫做first,第二个参数叫做second。
在这里插入图片描述
》如果我们想插入的话,map<string, string>dict;dict.insert(pair<string,string>(“sort”,“排序”));这是第一种,匿名对象的一种传参数,用起来比正常的要舒服很多。第二种,正常点是这样的,pair<string,string>kv(“insert”,“插入”);dict.insert(kv);还有第三种方式,用起来更爽:用到make_pair,make_pair就是制造一个pair对象,即dict.insert(make_pair(“left”,左边));
》前面第一、第二种都是借用的pair的构造函数来进行。第三种,make_pair是一个函数模版,它的优势是不用声明参数,让它自动推导。第四种,还能这么写,dict.insert({“right”,右边});虽然知道可以这么写,但是目前还是不太清楚为什么,这个涉及到C++11点类型转换,C++11再讲。
》map的遍历:map<string,string>::iterator it = dict.begin();我们也说过,如果你清楚的情况下可以用auto让它自己推导。while(it != dict.end())coyt << it << " ";++it;**发现不支持it**。有没有人想过,你直接分别单独用变量表示key和value不就好了嘛,为什么要封装一个pair?这个地方就能看出来,为什么要搞一个pair了。这个map的迭代器和链表的迭代器有些相似,它用自定义类型去封装,封装节点,只不过在map这里是一个树的节点,那么解引用是不是去调用operator呀,那C++是否支持一个函数返回两个值呢?这里it本质是it->operator(),它是不是要返回节点里面的数据呀,对于set还好,set里面只有一个key,但是map这里是一个key和value,那C++是否支持一次返回两个值呢?不支持是吧,但是你这里用到了模拟解引用的行为,那就要返回节点,如果你这里分别单独用变量表示key和value就没办法搞了,所以我把key和value给你打包成一个结构数据,这个结构数据就叫做pait<>类型,所以operator,返回的是节点数据,这个节点数据是一个pair<>类型。
》但是你这
类型pair<>支不支持流呢,即cout << it吗?答案是:不支持。有同学说,我们来支持一下就可以了,但是支持的话是不是就要改库里面的代码呀,那么我们换一种方法玩。你不是一个结构嘛,那么我们就是cout << (*it).first << “:” << (*it).second << endl;但是在这种地方,你的迭代器是不是指针的行为呀
,那你还用解引用*吗?不会,而是
用迭代器重载的->运算符来用*,即cout << it->first << “:” << it->second << endl;
》那这里的范围for,for(const auto& kv : dict),kv其实是返回的*解引用后的pair数据类型,用kv.first()和kv.second()来访问。

void test_map1()
{
	map<string, string> dict;

	// pair构造函数
	dict.insert(pair<string, string>("sort", "排序"));
	pair<string, string> kv("insert", "插入");
	dict.insert(kv);

	// make_pair
	auto ret1 = dict.insert(make_pair("left", "左边"));
	auto ret2 = dict.insert(make_pair("left", "剩余"));

	dict["operator"] = "重载"; // 插入+修改
	dict["left"] = "左边、剩余"; // 修改
	dict["erase"];  // 插入
	cout << dict["left"] << endl; // 查找


	// C++11 再讲
	//dict.insert({ "right", "右边" });

	// 遍历
	//map<string, string>::iterator it = dict.begin();
	auto it = dict.begin();
	while (it != dict.end())
	{
		//cout << *it << " "; // it->operator*()
		//cout << (*it).first << ":" << (*it).second << endl;
		cout << it->first << ":" << it->second << endl;
		++it;
	}
	cout << endl;

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

//mapped_type& operator[] (const key_type& k)
//{
//	pair<iterator,bool> ret = insert(make_pair(k, mapped_type()));
//
//	//return (*(ret.first)).second;
//	return ret.first->second;
//}

那你现在统计水果出现的次数是不是就很方便了呀,先构建一颗map树,map<string,int> countMap;for(aoto& str : arr)是不是就可以先调用map的find()函数,auto it = countMap.find(str);如果水果没有插入到我们的树里面,那么我们就构建一个pair<string,int>,即countMap.insert(make_pair(str,1));如果已经存在了,就it->second++;

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

	// 17:08继续
	/*map<string, int> countMap;
	for (auto& str : arr)
	{
		map<string, int>::iterator it = countMap.find(str);
		if (it != countMap.end())
		{
			it->second++;
		}
		else
		{
			countMap.insert(make_pair(str, 1));
		}
	}*/

	//map<string, int> countMap;
	//for (auto& str : arr)
	//{
	//	//pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(str, 1));
	//	auto ret = countMap.insert(make_pair(str, 1));
	//	if (ret.second == false)
	//	{
	//		ret.first->second++;
	//	}
	//}

	map<string, int> countMap;
	for (auto& str : arr)
	{
		countMap[str]++;
	}

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

int main()
{
	//test_set6();
	test_map1();

	return 0;
}

其实map这里最重要的就是刚刚讲的,那么刚刚上面讲的呢还有优化的空间,这个优化的空间是要帮我们去搞后面的事情。

map<string, int> countMap;
	for (auto& str : arr)
	{
		map<string, int>::iterator it = countMap.find(str);
		if (it != countMap.end())
		{
			it->second++;
		}
		else
		{
			countMap.insert(make_pair(str, 1));
		}
	}

这个代码其实是可以改进优化的。find()这里有这么一个问题,我去find()是不是一直去找,不在的话是不是我就相当于从根找到尾了呀,没有的话,我就得去插入,插入是不是又得去找一次,那是不是重复了呀,find()找一次,没找到后,insert又找了一次。其实不优化也无关痛痒,但是这里讲优化是为下面做铺垫,为下面的玩法做铺垫。
》所以这里就可以用insert()来做一件事情,正常的呢,insert()的返回值是一个pair<>类型的数据pair的first数据被设置成为一个iterator迭代器这个迭代器指向两种可能,要么指向新插入的元素(插入成功),要么指向与key相等的元素(插入不成功)。意味着是这样的,我insert()返回的pair<>类型的数据,first是一个迭代器,这个迭代器呢,一定是指向一个位置的,要么是你新插入元素的迭代器,要么是没有插入成功,因为反冗余的嘛,那么itreator就是已经有了的元素的迭代器。那么pair<>的second数据类型是一个bool,如果是新插入的,那就是true,如果已经有相等的了,就是flase。也就是说,它插入成功或者失败是会返回bool值的。我insert()返回的pair的迭代器不管你是插入成功还是失败都会返回一个元素的位置,那么就可以利用起来。
在这里插入图片描述
》就拿上面统计水果个树来说,如果水果已经存在map里面了,还会插入吗?不会。**这个待会儿对我们理解"[]"的使用非常重要 。**它insert()返回的pair类型的值非常有意义和我们待会儿要讲的“【】”有很大关系。我们插入atuo ret1 = dict.insert((“left”,左边))和auto ret2 = dict.insert((“left”,剩余)),按理来说第一次插入的left是成功的,ret1的second数据是true,first数据是新插入元素的迭代器;第二次插入left,ret2的second数据是flase,然后ret2的first数据和ret2一样的。
》我们就可以进行对上面代码改进,不需要再调用find()函数了,就调用insert()就行。我们不管有没有插入,都调用insert()函数,即pair<map<string,int>::iterator,bool> ret = countMap.insert(make_pair(str,1));写简单一点就是auto ret = countMap.insert(make_pair<str,1>);有两种情况,插入成功我用不用管,我不管,也就是第一次出现,那么返回的pair的second是一个true;插入失败,说明水果已经有了,那么insert()是不是同时充当了find()的作用,因为它把对应的元素的迭代器也给我了。 if(ret.second == false)那就是插入失败,已经有该水果了,所以ret.first->second++;

map<string, int> countMap;
	for (auto& str : arr)
	{
		pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(str, 1));
		auto ret = countMap.insert(make_pair(str, 1));
		if (ret.second == false)
		{
			ret.first->second++;
		}
	}

operator[]

》其实我们统计也不会用这个方法,这个方法太憋屈了,我们**讲这个真正的目的是为了玩“【】”**这个重载运算符,而且C++里面的map非常喜欢用“【】”,如果说map的话,我们重点学习的就是这个“【】”重载运算符。我们统计次数怎么统计,我先写好:

map<string, int> countMap;
	for (auto& str : arr)
	{
		countMap[str]++;
	}

此书成功统计出来,没有毛病,这个“【】”很绝。一句代码统计完次数!就用了operator []这么一个重载运算符。
在这里插入图片描述
》operator[]返回值是一个mapped_type&类型。我们想想,我们什么时候重载过operator[]了?我们在vector、string的时候重载了operator[],但是这里map支持随机访问吗?并不支持,目的也不是。
》其实这里map的operator[]已经改变了[]的意义了,它不是随机访问,它的参数也不是下标。它的参数是key,返回值是value的引用&。
》那么这里怎么做到一句话就统计了此书呢?insert()都没有了吗?我们来看看源码实现,
在这里插入图片描述
这个代码能把人看崩掉,我们简化一下代码,我们先提取出(this->insert(make_pair(K,mapped_type()))).first,这一串其实是谁呢?实际上就是调用了insert()嘛。
》insert的返回值是不是pair<map<>::iterator,bool>类型数据呀,即pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type())),然后它取了ret的first数据,first是个什么呢,不就是指向节点的迭代器嘛。
》所以一开始的代码就可以理解成

pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type()))
return (*(ret.first)).second;

》其实这个也绕了一点,其实等价于:

pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type()))
return (ret.first)->second;

我们在“【】”里面给的是key值,有两种情况:1.Key在map对象中;2.Key不在map对象中。这里是精华,很多面试中都会考这里。
》第一种,Key在map对象中,我们要进行插入,make_pair(k,mapped_type())中的mapped_type()是个啥?我们先来看看mapped_type是谁?是不是T类型,就是Value的类型呀,也就是第二个模版参数的类型,第一个模版参数的类型是key的类型,K嘛。那么mapped_type()是不是用Value类型进行构造的匿名对象呀!
》也就是说,你来了一个Key,我不管你在不在,我先insert(0。假设第一种情况,Key是存在的,那么插入失败,那么insert()也就会插入失败,那么你pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type()))中的ret的second数据bool类型就是flase!first数据就是Key所在节点的迭代器。然后return (*(ret.first)).second;可能早期没有支持“->”这个符号,所以我们现在支持,就这样写:ret.first->second------>这个是不是就是节点的Value值呀。并且不要忘了**,我operator[]的返回值,是Value的引用&!引用的作用是:1.减少拷贝;2.支持修改!**
》所以此时operato[]的作用有两层:1.查找K对应的Value;2.修改K对应的Value值。
》第一种情况,插入失败,insert()完全没有起作用,它反而充当的是查找的作用。它这里的operator[]返回值只用的是insert()返回值的first,即返回节点的迭代器。插入成功还是失败无所谓呀,为什么呢?如果Key在map对象中,肯定插入失败嘛;
》第二种情况,如果Key不在对象中,那么insert()是不是插入成功,operator[]的返回值是不是节点的迭代器中的Value值的引用&呀,这个值一开始是匿名构造的对象,上面有说(如果Value是stirng类型就是空串,如果是int类型就是0,如果是指针类型就是空nullptr)。我返回的Value值的引用,那也就意味着充当着 插入 和 修改两层作用,你可以修改,也可以不修改。
》我们再来分析一下,我们写的代码:

countMap[str]++;

水果要是第一次出现,那会先插入,插入的时候节点的个数是intt类型,它内部会匿名构造int类型对象就是0,但是operator[]会返回这个int的引用,我们在外面用了++,所以0会++;如果水果已经有了,它们operator[]不会插入成功,它会返回水果所在节点的Value值的引用,然后Value值++。
》我们再从另外一个角度,我们知道这个之后呢,有些人插入字典就不会像我们一开始那样写了,而是这样写:

auto ret1 = dict.insert(make_pair("left", "左边"));
auto ret2 = dict.insert(make_pair("left", "剩余"));
dict["operator"] = "重载"; // 插入+修改
dict["left"] = "左边、剩余"; // 查找 + 修改
dict["erase"];  // 插入
cout << dict["left"] << endl; // 查找

所以operator[]是一个多元化功能,map的其他函数参考set就行了。

multimap

它和map不同的是,它是允许冗余的,它是可以插入Key相同的键值对的,哪怕你的Value值相同也是可以的,其他的东西和map其实是一样的。

multimap<string,string> dict;
dict.insert(make_pair("left",“左边”));
dict.insert(make_pair("left",“剩余”))

》相比map ,multimap使用函数接口最大的区别是什么?multimap不支持operator[];为什么呢?如果对于map你给一个Key,我是不是要么有,要么没有,有的话,我就返回对应的Value,没有的话我好可以插入。但是mutiple有多个Key的时候,那么Key是不是面临一对多的关系呀,所以不支持operator []也是情理之中的。

题目:前K个高频单词

这个题目是一个统计次数➕topK的问题。我们不管三七二十一,先把各个单词出现的次数统计出来,当然可以用map来帮我们做KV映射,map<string, int> countMap;然后将words遍历一遍就可以得到各个单词出现的次数。
》接下来就是TopK的问题了。我们以前应该是讲过,如果建堆来解决,我们是不是要建小堆来解决是吧,但是我们不考虑用堆来解决,其实优先级也就是堆,因为优先级队列就是用堆来封装的。因为如果用堆的话,选出K个之后还需要进行排队。
》我们的map<string, int>它所插入的值,是按照string来进行排序的,但是我们要的是按int来进行排序。sort()能对map进行排序吗?答案是:不能!因为sort()要求的是随机迭代器,map显然不满足。那我们是不是要将map里面的值放到vector里面,然后就可以用sort()来进行排序了呀,当然vector存放的元素类型就是pair<string, int>了呀,即vrctor<pair<string, int>> vSort;
》当然也可以不放pair类型,而是vector<map<string, int>::iterator> v;这和我们放pair<string, int>有什么区别呢?这种方案行不行呢? 大家想想待会儿用sort()是不是必然要自己控制比较方式呀,如果要自己控制比较方式的话,你放pair<>我要写仿函数,我放迭代器你是不是也得控制比较方式呀。大家想一想,我在vector里面放迭代器iterator后,能不能访问到里面的Key和Value?是可以的。但是区别在哪里呢?如果你放的是pair<string, int>你待会儿得拷贝,把map里面的每一个数据都拷贝到pair里面,而我iterator迭代器就是一个指针,所以针对于拷贝的代价,我们还是选择迭代器iterator比较好。
》由于迭代器写出来很长,我们先typedef一下,typedef map<string, int>::iterator countIrer;然后我们就是遍历map,将每一个节点的iterator都插入到vector里面。
》接下来就是用到sort()函数进行排序了。 sort(v.begin(), v.end())这个时候能不能支持排序呢?是不能支持到,因为vector的迭代器v.begin()解引用之后是map节点的迭代器,那么它支持排序吗?不支持。不支持怎么办?我们是不是得写一个仿函数来控制排序使其支持呀。
》所以我们来写一个仿函数,struct IteCompare{}在其里面支持一个bool operator() (countIter ite1, countIter ite2)这里是支持迭代器进行比较。大家想清楚,因为vector里面的每一个数据是迭代器,我们将vector的迭代器传给sort,sort解引用*是map节点pair的迭代器,所以是迭代器比较;那么迭代器比较,要用仿函数来控制比较并使其能支持比较,所以我们写了bool operator() (countIter ite1, countIter ite2),然后函数体内写:return ite1->second > ite2->second;然后我们的仿函数就完成了。
》接下来就是将仿函数一并传给sort(),即sort(v.begin,v.end(),IterCompare);记住IterCompare是类型,我们应该传一个对象给sort(),所以是用到匿名对象IteComapre(),即sort(v.begin,v.end(),IterCompare());
》然后我们排序好了,我们还得取出前K个,是不是还得再来一个vector ret;然后将排完序后的vector循环K次呀,并且将值push_bacnk()进我们的ret里面。
》但是光光这样还是不能提交成功,因为我们的选出来的K个words单词顺序不对,因为如果单词出现的次数相同的话,还得进行ASICC码排序,将出现次数相同的单词进行排序。
》按道理,一开始我们的countMap是按string的大小(也就是按照Key去排序的)进行将每一个节点插入的,所以一开始就是将按string将words排序好了,然后紧接着我们再遍历将每一个节点的迭代器放入vector了呀,此时也是按照words的string来进行排序的,但是我们后面用了sort()之后,sort()是一个不稳定的排序,它会将我们原来的words正确顺序给打乱的。所以,你sort()完之后,选出来了K个,但是words的顺序你还得再排一次,除非你用一个稳定的排序。
》在sort()文档里面它有告诉你,它sort()是一个不稳定排序,它提供了一个stable_sort()是能够保证稳定性的。所以,我们将sort()换成stable_sort()试一下能不能通过,是能通过的。
》或者还有方法,你如果提前考虑到了这点的话,你可以在仿函数阶段也将words的顺序也控制好,即(ite1->second > ite2->second || ite1->second == ite2->second && ite1->first > ite2->first),所以我认为次数大的肯定大,如果次数相等,再比较words来进行排序。这样就可以调用sort()了。这不是改变sort()的稳定性,而是改变了比较的规则。但是会有一些性能的影响。
》我们还有别的方法,如果要排序,除了用sort(),是不是map本身也支持排序呀,只不过一开始是对string来排序的,我再定义一个map<int, string> sortMap;那么就是遍历一遍countMap,然后sortMap.insert(make_pair(kv.second,kv.string));就是将K和V调个位置,但是map会进行去重,所以我们还得用mutilmap才行,即mutilmap<int, string> sortMap;然后我们再去sortMap里面去取元素就行了,即for()循环K次,然后ret.push_back(it->second) ++it。但是取出来K个之后,它是按升序来的,显然不符合我们的要求,此时我们的map、mutilmap都是支持自己定义比较方式的,这不就体现出来了吗,所以,我们在定义sortMap的时候,还得传入另外一个参数,mutilmap<int, string, greater> sortMap;要传入一个greater来进行控制比较方式。
》最靠谱的还是sort或者srable_sort,因为如果用map的太依赖其底层的实现了。
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

g++ 编译选项

1&#xff0c;基本编译过程 g可以用于编译C代码生成可执行程序&#xff0c;从原始代码到生成可执行过程中实际经历了以下4个步骤&#xff1a; 1. 预处理&#xff1a;宏替换&#xff0c;注释消除&#xff0c;查找相关库文件等[使用-E参数]。 # 只激活预处理&#xff0c;不会自…

集成正态云和动态扰动的哈里斯鹰优化算法(IHHO)-附代码

集成正态云和动态扰动的哈里斯鹰优化算法(IHHO) 文章目录 集成正态云和动态扰动的哈里斯鹰优化算法(IHHO)1.哈里斯鹰优化算法2.改进哈里斯鹰优化算法2.1 正态云模型2.2 随机反向学习思想2.3 动态扰动策略 3.实验结果4.参考文献5.Matlab代码6.python代码 摘要&#xff1a; 针对基…

Uni-app学习从0到1开发一个app——(3)简单小工程内容介绍

文章目录 工程文件 看看一个标准的hello微信小程序工程文件的组成和作用。 工程文件 可以参考官方教程&#xff1a;传送门 之前的文章有详细的开发环境介绍&#xff0c;传送门Uni-app学习从0到1开发一个app——(2)windowns环境搭配&#xff0c;这里我们先建一个简单的示例微信…

【工具】Xshell-7和Xftp-7下载安装使用教程

目录 一、Xshell和Xftp 二、安装包下载(Xshell和Xftp) 三、Xshell安装、使用和常用设置 1. Xshell安装&#xff1a; 2. Xshell使用: 3. Xshell常用设置 三、Xftp安装、使用 1. Xftp安装 2. Xftp使用 一、Xshell和Xftp Xshell&#xff1a; Xshell是一款强大的SSH&#xff…

【数据结构与算法分析】树上漫步之探究前序、中序、后序、广度优先遍历算法的实现与优化

文章目录 前言二叉树的遍历方式构建二叉树递归遍历二叉树非递归遍历二叉树层次遍历 示例二叉树结果总结 前言 二叉树是数据结构中最基本的数据结构之一&#xff0c;它在计算机科学中有着非常重要的应用。二叉树的遍历是指按照一定的顺序遍历二叉树中的所有节点&#xff0c;是二…

DML——数据库查询语言

查询——select SELECT [DISTINCT/ALL/] {*|column|expression [alias],…} FROM table [Natuarl join /] where子句; Natuarl join 自然连接只考虑那些在两个关系模式中都出现的属性上取值相同的元祖队。 列名&#xff08;属性名&#xff09;完成相同值相同去除重复列拓展&…

【Typora+Lsky】在deepin使用YGXB-net/lsky-upload上传图片

本文首发于 慕雪的寒舍 在win和deepin上使用lsky-upload上传图片 1.说明 先前使用lsky图床的时候&#xff0c;我一直用的是picgo的插件来上传图片。 但最近picgo总是遇到卡上传的问题 https://github.com/Molunerfinn/PicGo/issues/1060 后来在gitee上面搜到了这个项目&…

Django实现接口自动化平台(七)数据库设计

上一章&#xff1a; Django实现接口自动化平台&#xff08;六&#xff09;httprunner&#xff08;2.x&#xff09;基本使用【持续更新中】_做测试的喵酱的博客-CSDN博客 下一章&#xff1a; 一、数据库设计 接口自动化平台&#xff0c;内置引擎&#xff0c;使用的是httprun…

【LeetCode热题100】打卡19天:最大数组和跳跃游戏

文章目录 【LeetCode热题100】打卡第19天&#xff1a;最大数组和&跳跃游戏⛅前言 最大数组和&#x1f512;题目&#x1f511;题解 跳跃游戏&#x1f512;题目&#x1f511;题解 【LeetCode热题100】打卡第19天&#xff1a;最大数组和&跳跃游戏 ⛅前言 大家好&#xff…

A股市场全景分析系列—从每日涨停个股分析热门板块与题材

前言‍‍ 当前市场股票多但资金有限&#xff0c;因此已经无法出现全面上涨的行情。这样一来识别当前的“风口”显得尤为重要&#xff0c;也就是上车热门板块、热门题材、强势个股&#xff01; 因此聚焦分析涨停板个股显得尤为重要&#xff01; 我们统计了近&#xff15;个交易日…

【算法题解】38. 括号的生成

这是一道 中等难度 的题 https://leetcode.cn/problems/generate-parentheses/ 题目 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;["…

DNS隧道穿透

介绍&#xff1a; DNS隧道&#xff0c;是隧道技术中的一种。当我们的HTTP、HTTPS这样的上层协议、正反向端口转发都失败的时候&#xff0c;可以尝试使用DNS隧道。DNS隧道很难防范&#xff0c;因为平时的业务也好&#xff0c;使用也罢&#xff0c;难免会用到DNS协议进行解析&am…

线上问题记录20230610-NGINX代理大文件下载问题

线上问题记录20230610-NGINX代理大文件下载问题 问题描述 我们有一个在线项目&#xff1a;是一个对象文件存储服务&#xff0c;是用来给用户提供文件对象存储的。今天用户在使用的时候发现超过1G的文件无法进行下载。幸好今天是星期六&#xff0c;使用的人并不是特别多&#…

华为OD机试真题 JavaScript 实现【最长的连续子序列】【2022Q4 100分】

一、题目描述 有N个正整数组成的一个序列&#xff0c;给定一个整数sum&#xff0c;求长度最长的的连续子序列使他们的和等于sum&#xff0c;返回该子序列的长度&#xff0c;如果没有满足要求的序列返回-1。 二、输入描述 第1行有N个正整数组成的一个序列。 第2行给定一个整…

【LIN通讯出现问题】

LIN信号发出了&#xff0c;但是没有收到响应的原因 如果你发送了LIN&#xff08;局域网互连&#xff09;信号&#xff0c;但没有收到响应&#xff0c;可能有以下几个原因&#xff1a; 线路故障&#xff1a;检查信号线路是否连接正确&#xff0c;可能存在接触不良、开路或短路等…

【PCB专题】Allegro 单线、差分线自动绕等长功能介绍

在文章【PCB专题】案例:绕等长怎么直接以颜色区分看出是否绕好 中我们讲到Allegro 16.6版本的Timing Vision功能可以直接在PCB上以颜色的区分就能看出单线和差分是否已经绕成等长了,不再需要到规则管理器中去查看。 那么其实Allegro还有单线自动绕等长的功能——Auto interac…

mac电脑储存内存越来越小如何清理释放空间?

如果你是一位Mac系统的用户&#xff0c;可能会发现你的电脑储存空间越来越小。虽然Mac系统设计得非常优秀&#xff0c;但是系统数据和垃圾文件也会占据大量的储存空间。在这篇文章中&#xff0c;我们将探讨mac系统数据怎么这么大&#xff0c;以及mac清理系统数据怎么清理。 一…

# Telegraph-Image:利用Cloudflare Pages和Telegraph无成本创建自己的图床

Telegraph-Image&#xff1a;利用Cloudflare Pages和Telegraph无成本创建自己的图床 Telegraph-Image是一个具有以下特点的图像托管服务&#xff1a; 特点&#xff1a; 无限图片储存数量&#xff1a;你可以上传不限数量的图片&#xff0c;没有存储限制。免费托管&#xff1a…

chatgpt赋能python:Python中如何删除字符串中的标点符号

Python中如何删除字符串中的标点符号 概述 在Python中&#xff0c;字符串是一种基本数据类型&#xff0c;经常被用来存储和处理文本数据。在处理文本数据时&#xff0c;我们常常需要删除其中的标点符号。本文将介绍如何使用Python中的字符串处理方法来删除字符串中的标点符号…

[安卓广播入门][1]Android Studio接收系统广播

一、新建项目 二、增加权限 <uses-permission android:name"android.permission.ACCESS_NETWORK_STATE" />三、代码 public class MainActivity extends AppCompatActivity {private IntentFilter intentFilter;//过滤隐式意图private NetworkChangeReceiver…