C++ STL:泛型算法lower_bound用于关联容器set等的坑
1.问题导入(背景):
我在写某道程序设计竞赛题的时候需要对set里面的元素(类型为pair<int,int>)进行查找,众所周知set自带lower_bound函数。但是我的需求是比较pair<int,int>的大小时优先考虑second的大小。而set自带的lower_bound函数无法传入一个自定义的比较函数,只能基于元素默认的比较方法(对于pair<int,int>默认优先考虑first的大小,first大小相同时才比较second的大小)。于是我就想,泛型函数lower_bound可以传入一个函数指针实现自定义比较函数(和往sort里传一个函数指针一样),那么我用泛型函数去进行二分查找不就好了吗(代码为lower_bound(S.begin(),S.end(),myCmp),其中S为set<pair<int,int>>型,myCmp是自定义的一个函数)。
2.发现异常:
然而,时间复杂度为nlogn的算法竟然在1e5范围的数据上超时了。
3.问题解决:
我的下意识解决方案是泛型函数lower_bound可能不适合用于set中(以往都是用在vector或者数组上的),于是我将原来的pair<int,int>的first和second调换了一下,并且利用set自带的lower_bound函数实现二分查找,最终成功ac。(将first和second调换的意思是:例如我用pair<int,int>存一个区间的左右端点,一开始我的first存的是左端点l,second存的是右端点r,现在我交换一下,first存的r,second存的l,这样无论是之前未交换时调用泛型lower_bound并且采用自定义myCmp还是交换后调用set自带的lower_bound,都是优先基于r比较)。当然还有另外的解决方法,例如不使用pair<int,int>,而自定义一个结构体/类,并且重载“operator<运算符”。
4.进一步探索:
虽然成功ac了,但是我好奇泛型函数lower_bound用于set为什么会导致超时。于是首先我通过搜索引擎搜索“泛型函数lower_bound用于set”等关键词,没有搜到相关结果(这也是为什么我要写这一篇博客),然后我去翻阅书籍:《C++Primer(第5版)》,最终找到相关表述(见下图),同时我通过搜索引擎搜索“泛型函数lower_bound用于关联容器",找到了一篇和书上内容完全相同的博客(链接:https://blog.csdn.net/KCDCY/article/details/123065792)
(关于为什么这里截取博客的图而不是用书上的图,因为我没有书本的电子版,而且也不想放照片在这篇博客里,上图应该挺好的。)
解释:泛型算法不建议用于关联容器,可以理解为因为关联容器不支持像数组和vector那样的随机访问,所以使用泛型搜索算法会导致线性查找(我的猜测)
5.实验验证:
经过上面《C++Primer(第5版)》对“泛型函数不建议用于关联容器”的解释,我猜测对关联容器实验泛型搜索算法会导致线性查找,于是进一步做实验验证:
运行下述代码:
1 //exeCreate 2022-5-15 10:35:11 2 3 #include<stdio.h> 4 #include<stdlib.h> 5 #include<iostream> 6 #include<algorithm> 7 #include<string> 8 #include<string.h> 9 #include<cmath> 10 #include<vector> 11 #include<set> 12 #include<queue> 13 #include<ctime> 14 15 #define rep(i,a,b) for(int i=(a);i<=(b);++i) 16 #define per(i,a,b) for(int i=(a);i>=(b);--i) 17 #define fi first 18 #define se second 19 #define mp make_pair 20 #define all(x) x.begin(),x.end() 21 22 using namespace std; 23 typedef long long ll; 24 typedef pair<ll,ll> PII; 25 typedef pair<int,int> Pii; 26 const int maxn=2e5+10,maxm=maxn; 27 const ll mod=1e9+7,inf=0x3f3f3f3f; 28 29 set<int> S; 30 int main() 31 { 32 int n=1e4; 33 rep(i,1,n){ 34 S.insert(i); 35 } 36 37 clock_t st,ed; 38 st = clock(); 39 rep(i,1,n){ 40 auto iter = S.lower_bound(i); 41 } 42 ed = clock(); 43 printf("STL:set自带lower_bound算法费时:%lfs\n",(double)(ed-st)/CLOCKS_PER_SEC); 44 45 st = clock(); 46 rep(i,1,n){ 47 auto iter = lower_bound(S.begin(),S.end(),i); 48 } 49 ed = clock(); 50 printf("lower_bound泛型算法费时:%lfs\n",(double)(ed-st)/CLOCKS_PER_SEC); 51 52 53 fflush(stdin); getchar(); 54 return 0; 55 } 56 //maxn改好了么?
运行结果:
STL:set自带lower_bound算法费时:0.000000s
lower_bound泛型算法费时:1.074000s
=====
Used: 1060 ms, 1512 KB
可见两个lower_bound的时间效率差距之大,可以认为后者是线性查找,所以测试代码中后者的部分(45-50行)的时间复杂度是O(n2)的(也就解释了为什么1e4的数据要1秒才能跑完)
6.结论与展望:
本文探讨了将泛型函数用于关联容器的糟糕(例子为泛型lower_bound用于set),通过查阅书籍(《C++Primer(第5版)》)猜测将泛型函数用于关联容器将导致线性查找(总之运行效率很低),并通过代码大致验证了这个猜想。记录下本篇博客是因为没有搜到讲解相关问题的博客,希望能被更多有相关困惑的人看到。本文对原理的探索是比较浅显的,更进一步需要阅读STL的源码来了解泛型函数对关联容器的具体操作。
最后说一句:通过这次经历我又双叒叕体会到了“系统地学习”,“广泛地学习”,“积累知识”的重要性,个人对于C++的语法特性尤其是STL是比较感兴趣的,但因为忙于其他事业并没能很好地系统学习(虽然以前基本读完了某本教材),《C++Primer(第5版)》在我现在看来是一本宝藏,希望我能坚持抽空将其系统地学习。