1. 程式人生 > 其它 >C++ STL:泛型演算法lower_bound用於關聯容器set等的坑

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那樣的隨機訪問,所以使用泛型搜尋演算法會導致線性查詢(我的猜測)

4.實驗驗證:

經過上面《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秒才能跑完)

5.結論與展望:

本文探討了將泛型函式用於關聯容器的糟糕(例子為泛型lower_bound用於set),通過查閱書籍(《C++Primer(第5版)》)猜測將泛型函式用於關聯容器將導致線性查詢(總之執行效率很低),並通過程式碼大致驗證了這個猜想。記錄下本篇部落格是因為沒有搜到講解相關問題的部落格,希望能被更多有相關困惑的人看到。本文對原理的探索是比較淺顯的,更進一步需要閱讀STL的原始碼來了解泛型函式對關聯容器的具體操作。

最後說一句:通過這次經歷我又雙叒叕體會到了“系統地學習”,“廣泛地學習”,“積累知識”的重要性,個人對於C++的語法特性尤其是STL是比較感興趣的,但因為忙於其他事業並沒能很好地系統學習(雖然以前基本讀完了某本教材),《C++Primer(第5版)》在我現在看來是一本寶藏,希望我能堅持抽空將其系統地學習。