除了內建的演算法函式外,STL還提供了許多容器。
所謂容器可以理解成拿來裝大量資料的工具,並且不同容器對資料有不同處理方式,各有不同功能。
如果能熟悉這些容器,就能利用其功能方便地實現許多複雜的功能。
由於這些容器都各自有許多功能,在介紹這些容器與功能前先大略描述一下如何使用這些功能。
在C++中較高級的型別有所謂「成員」的概念,包括成員變數、成員函式,可以理解為這些比較強的型別可以在其底下開變數或者專屬的函式,使用方式為在高級型別的變數後加上「.成員變數名稱」或「.成員函式名稱(參數)」,之前的pair的.first、.second跟string的.size()分別就是成員變數與成員函式的概念。
接著以下舉出幾項常用的容器進行介紹。
vector<型別>
簡介:類似陣列,但提供更靈活的大小及各種方便的操作。vector並沒有固定大小,最初宣告時大小為0,隨著加入、刪除資料可以不斷改變大小。
常用功能與說明(設vector變數名稱為v,而以下提到的元素型別皆代表vector<型別>中的型別):
用法 | 複雜度 | 功能說明 |
---|---|---|
v[i] | O(1) | 取第i項元素,回傳元素型別 |
v.push_back(元素型別) | O(1) | 新增元素到v末端,void無回傳 |
v.pop_back() | O(1) | 移除v最末端的元素,void無回傳 |
v.back() | O(1) | 回傳v最末端的元素,等同v[v.size()-1],回傳元素型別 |
v.clear() | O(n) | 清空v中的所有元素,void無回傳,複雜度之n為原本v的大小 |
v.size() | O(1) | 回傳v內的元素數量,回傳int |
v.empty() | O(1) | 回傳v是否為空,回傳bool |
#include<bits/stdc++.h>
using namespace std;
int main(){
vector<int> v;//宣告時v為空
if(v.empty())cout<<"v is empty\n";//輸出「v is empty」
v.push_back(1);
v.push_back(2);
v.push_back(3);
//依序在末端新增1,2,3
cout<<"v has "<<v.size()<<" elements\n";//輸出「v has 3 elements」
for(int i=0;i<v.size();i++)cout<<v[i]<<' ';//輸出「1 2 3」
cout<<'\n';
v.pop_back();//移除末端的元素,即3,v變為「1,2」
cout<<"v has "<<v.size()<<" elements\n";//輸出「v has 2 elements」
for(int i=0;i<v.size();i++)cout<<v[i]<<' ';//輸出「1 2」
cout<<'\n';
}
注意vector中取到索引值大於等於v.size()會導致程式掛掉,在v.size()為0時執行v.back()或v.pop_back()也會導致程式掛掉,因此執行這些函式時,若有可能發先v為空的情況請利用條件判斷進行處理。
要使用vector儲存輸入的陣列有兩個方式,一是每次輸入a後v.push_back(a),建議用此種方式即可,另一種是用以下補充的產生特定大小、初始值的vector來宣告。
與pair<int,int>(5,10)回傳一個內容為「5,10」的pair類似,在vector<型別>後打上括號,可以填入兩個數字分別為大小、初始值(也可只填大小,此時初始值自動帶0),如「vector<int>(100,1)」會回傳包含100個1的vector、「vector<int>(10)」回傳包含10個0的vector,若搭配此進行宣告可使用auto。
另外由於vector功能更強了,因此也有提供比陣列的指標更強的工具,稱作迭代器(iterator),不過此時就不能使用一般的指標了,因為資料是儲存在容器中,需要用容器提供的專門方式來存取,雖然迭代器是新的名詞,但基本上可以理解為STL版的指標。
其宣告方式為vector<型別>::iterator,很複雜對吧,所以這時候之前提到的自動判斷型別auto就派上用場了,如v.begin()會回傳指向v[0]的iterator,此時用「auto it=v.begin();」就可以宣告vector<型別>::iterator了。
而由於迭代器與指標十分類似,因此一樣有「it+n可以得到it向後n項」、「it1-it2可以得到相差幾位」、「it++、it--可以將it向前、向後移動」的規則都適用,當然也一樣需要注意避免將迭代器操作到vector範圍外
除了上述提到v.begin()會回傳指向v[0]的迭代器,還有v.end()會回傳指向v末端的迭代器,但請注意這個末端是沒有儲存資料的,最後一項資料儲存在v.end()-1,其實這與前述STL的範圍會以[first,last)前閉後開的方式處理有關,v實際存值的範圍為[ v.begin(),v.end() ),如果你認為這很複雜,不妨想想要排序整個v時要怎麼寫,答案是「sort(v.begin(),v.end());」,仔細想想就會發現其實都能對應起來。
#include<bits/stdc++.h>
using namespace std;
int main(){
vector<int> v;
for(int i=0;i<10;i++)v.push_back(i);//v之內容為「0,1,2,...,9」
cout<<"v.begin() is "<<*v.begin()<<'\n';//輸出「v.begin() is 0」
cout<<"v.end()-1 is "<<*(v.end()-1)<<'\n';//輸出「v.end()-1 is 9」
//因v.end()無存值,屬v之範圍外,執行如*v.end()的操作會導致程式掛掉
for(auto it=v.begin();it!=v.end();it++)cout<<*it<<' ';//輸出「0 1 2 ... 9」
//考慮先前章節0-2對for的解讀,it會從v.begin(),每次向後跑一位,直到it!=v.end()不符合,此時it指向v的範圍外,直接跳出迴圈就不會造成程式掛掉
}
看到這裡也許有的讀者會好奇迭代器還有什麼用途,其實迭代器很多時候是搭配前述lower_bound、upper_bound兩個二分搜尋函式使用,這點在後續練習題目時就會漸漸遇到。
如果對迭代器的操作、應用仍不熟悉也沒關係,可以先行閱讀後續內容,待實際需要使用到時再回來複習。
另外在vector的最後也要來介紹一種遍歷(全部跑過一遍稱為遍歷)vector的簡易寫法,算是有點偷懶的方法,並且不如用索引值操作來得有彈性,因此建議還是先熟悉利用索引值遍歷,此方法可以看看就好。
#include<bits/stdc++.h>
using namespace std;
int main(){
vector<int> v;
for(int i=0;i<10;i++)v.push_back(i);
for(int i:v)cout<<i<<' ';//輸出「0 1 2 ... 9」
cout<<'\n';
//以上程式會將v從頭到尾跑過一遍,並將每個元素賦值給新變數i來執行迴圈內容
//我習慣念其中的:為in,因為int i in v解讀成「在v內的int i」會好懂很多
for(int &i:v)i*=2;
cout<<v[5];//輸出「10」
//與章節1-2提到的一樣,宣告時加&為取別名,此時變數i等價於v中元素,會將v中的元素逐個以i為別名執行迴圈內容
//以上for迴圈內的宣告皆可用auto取代
}
queue<型別>
簡介:queue的中文意思之一為排隊,也就是丟進去的資料會開始排隊,接著可以從最前端也就是最早排進去的元素開始取出。
常用功能與說明(設queue變數名稱為q,而以下提到的元素型別皆代表queue<型別>中的型別):
用法 | 複雜度 | 功能說明 |
---|---|---|
q.push(元素型別) | O(1) | 新增元素到q末端,void無回傳 |
q.pop() | O(1) | 移除q最前端的元素,void無回傳 |
q.back() | O(1) | 回傳q最末端的元素,回傳元素型別 |
q.front() | O(1) | 回傳q最前端的元素,回傳元素型別 |
q.clear() | O(n) | 清空q中的所有元素,void無回傳,複雜度之n為原本q的大小 |
q.size() | O(1) | 回傳q內的元素數量,回傳int |
q.empty() | O(1) | 回傳q是否為空,回傳bool |
其實沒有必要特別了解什麼是關聯容器,只要知道使用方式就可以把它當一般容器來用了。
之所以將關聯容器獨立出來講,是由於其某些特性與前述的一般容器不同,前面的所講的容器正式的稱呼為「序列容器(sequence container)」,儲存資料時重視存入的順序;相較之下「關聯容器(associative container)」儲存資料時不在乎存入順序,只在乎如何高效率的儲存、尋找、取得存入容器的資料。
以下介紹常見的關聯容器。
set<型別>
簡介:set的意思之一為數學上的「集合」,數學上的集合是一個可以有任意數量不重複元素的群體,STL set同樣可以儲存許多不重複的元素,並且對元素的儲存方式是始終保持排列好的狀態。
常用功能與說明(設set變數名稱為s,而以下提到的元素型別皆代表set<型別>中的型別,複雜度的n皆為操作當下s的元素數量):
用法 | 複雜度 | 功能說明 |
---|---|---|
s.insert(元素型別) | O(log n) | 插入或者說新增元素到s,若s內已有相同元素,則不再插入、s不變,void無回傳 |
s.erase(元素型別) | O(log n) | 移除指定元素,若s內沒有指定的元素,則s不變,void無回傳 |
s.count(元素型別) | O(log n) | 回傳s內指定元素的數量,由於s之元素不會重複,故回傳只有0、1,可當作查詢s內是否有指定元素,回傳int |
s.clear() | O(n) | 清空s中的所有元素,void無回傳,複雜度之n為原本s的大小 |
s.size() | O(1) | 回傳s內的元素數量,回傳int |
s.empty() | O(1) | 回傳s是否為空,回傳bool |
#include<bits/stdc++.h>
using namespace std;
int main(){
set<int> s;
s.insert(5);
s.insert(1);
s.insert(3);
s.insert(3);
//依次插入5、1、3、3,但由於set不儲存重複元素,故3僅儲存一次
cout<<"s has "<<s.size()<<" elements\n";//輸出「s has 3 elements」
for(int i:s)cout<<i<<' ';//STL set儲存方式為將元素排列,輸出「1 3 5」
cout<<'\n';
//遍歷set元素的方式有二,一是如vector遍歷的快速寫法一樣,只不過由於改變set的元素會導致儲存順序改變,因此不允許改變set之元素,也就不能使用int &i的方式來為s之元素取別名
//方式二是利用迭代器,後續再進行介紹
s.erase(1);
cout<<"s has "<<s.size()<<" elements\n";//輸出「s has 2 elements」
for(int i:s)cout<<i<<' ';//STL set儲存方式為將元素排列,輸出「3 5」
cout<<'\n';
cout<<s.count(1)<<' '<<s.count(3);//輸出「0 1」
}
如同範例程式1內提到的,set同樣有迭代器,只不過由於set儲存方式較為複雜,並不像vector為連續記憶體,因此迭代器的使用有些不同。
最大的差異在於不能直接加減位數來得到特定元素的迭代器,也沒有+=、-=,但仍能使用++、--來取得下一個、上一個元素,此外由於++、--必須針對自行宣告的迭代器變數使用,且會改變變數,因此這邊補充next(迭代器)跟prev(迭代器)這兩個會回傳下一個、上一個迭代器的內建函式。
另外在set中比vector更需要注意不能出現對迭代器的非法操作,如迭代器等於s.begin()時不能再跑到上一個、等於s.end()時不能再跑到下一個,以及不能對沒有值的迭代器取值即不能*s.end()、s為空時不能*s.begin()、*prev(s.end()),否則程式會馬上掛掉。
以下繼續介紹與set迭代器相關的函式,由於set之儲存方式並非連續記憶體,因此先前許多STL函式不能直接應用在set上,為此set提供了類似的成員函式。
用法 | 複雜度 | 功能說明 |
---|---|---|
s.begin() | O(1) | 回傳s起始位置也就是最小元素的迭代器 |
s.end() | O(1) | 回傳s結束位置的迭代器,此處實際上沒有儲存資料,s之最大元素在prev(s.end()) |
s.find(元素型別) | O(log n) | 回傳指定元素的迭代器,若指定元素不在s裡則回傳s.end() |
s.lower_bound(元素型別) | O(log n) | 回傳在s中,順序在指定元素後(或等於指定元素)的元素的迭代器 |
s.upper_bound(元素型別) | O(log n) | 回傳在s中,順序在指定元素後(不等於指定元素)的元素的迭代器 |
#include<bits/stdc++.h>
using namespace std;
int main(){
set<int> s;
s.insert(5);
s.insert(1);
s.insert(3);
s.insert(3);
for(auto it=s.begin();it!=s.end();it++)cout<<i<<' ';//輸出「1 3 5」
cout<<'\n';
}
如果對前述STL函式夠熟悉,應該會記得sort也是將資料進行排序,複雜度為O(n log n)。
但使用set進行n次insert,會得到同為O(n log n)的複雜度(雖然看似是log 1+log 2+...+log n,但由於log增長速度為遞減,大部分項目的數值會接近log n,總和趨近n log n)。
那是否可以用set來替代sort呢?
答案是不建議,因為最初介紹複雜度時有提到我們會把其係數消掉,因此雖然複雜度看似一樣,但實際上可能會有數倍甚至數十倍的時間差異,而set由於其功能強、實作細節複雜,操作花費實際時間略長,最後效率是較sort差許多的。
但如果估算結果(數字帶入,log部分用以2為底的log2計算)低於108則可以考慮在set較易實作時用set偷懶。
multiset<型別>
簡介:允許重複元素的set。
常用功能與說明(設multiset變數名稱為ms,而以下提到的元素型別皆代表multiset<型別>中的型別,複雜度的n皆為操作當下ms的元素數量):
用法 | 複雜度 | 功能說明 |
---|---|---|
ms.insert(元素型別) | O(log n) | 插入或者說新增元素到ms,不管該元素是否重複,void無回傳 |
ms.erase(元素型別) | O(k+log n) | 移除指定元素,若有多個元素會一次全部移除,void無回傳,複雜度之k為指定元素數 |
ms.erase(迭代器) | O(1) | 移除指定迭代器的元素,可以用ms.erase(ms.find(元素型別))來刪除單一元素、避免全部刪除,void無回傳,在已經有迭代器的情況為O(1) |
ms.count(元素型別) | O(k+log n) | 回傳ms內指定元素的數量,回傳int,複雜度之k為指定元素數 |
ms.clear() | O(n) | 清空ms中的所有元素,void無回傳,複雜度之n為原本ms的大小 |
ms.size() | O(1) | 回傳ms內的元素數量,回傳int |
ms.empty() | O(1) | 回傳ms是否為空,回傳bool |
ms.begin() | O(1) | 回傳ms起始位置也就是最小元素的迭代器 |
ms.end() | O(1) | 回傳ms結束位置的迭代器,此處實際上沒有儲存資料,ms之最大元素在prev(ms.end()) |
ms.find(元素型別) | O(log n) | 回傳第一個指定元素的迭代器,若指定元素不在ms裡則回傳ms.end() |
ms.lower_bound(元素型別) | O(log n) | 回傳在ms中,順序在指定元素後(或等於指定元素)的元素的迭代器 |
ms.upper_bound(元素型別) | O(log n) | 回傳在ms中,順序在指定元素後(不等於指定元素)的元素的迭代器 |
#include<bits/stdc++.h>
using namespace std;
int main(){
multiset<int> ms;
ms.insert(5);
ms.insert(1);
ms.insert(3);
ms.insert(3);
for(int i:ms)cout<<i<<' ';//輸出「1 3 3 5」
cout<<'\n';
ms.erase(3);
for(int i:ms)cout<<i<<' ';//輸出「1 5」
cout<<'\n';
//若改為ms.erase(ms.find(3));,則只會刪除一個,輸出會是「1 3 5」
}
map<型別1,型別2>
簡介:map在此處翻作「映射」,功能類似set但更加強大,除了可以儲存不重複的值(稱作鍵值key,型別為宣告的型別1),更可以將這些值對應到另一個資料(稱作數值value,型別為宣告的型別2),內部的儲存方式類似儲存多個pair<型別1,型別2>的set,但與set完全不能直接修改元素不同,map的型別2元素是被對應到的數值,可以改動。
常用功能與說明(設map變數名稱為m,而以下提到的元素型別1、2皆代表map<型別1,型別2>中的型別1、2,複雜度的n皆為操作當下m的元素數量):
用法 | 複雜度 | 功能說明 |
---|---|---|
m[元素型別1] | O(log n) | 取得key對應的value,回傳元素型別2 |
m.insert({元素型別1,元素型別2}) | O(log n) | 插入或者說新增一組key與value的pair到m,若key重複會將value更新,void無回傳 |
m.erase(元素型別1) | O(log n) | 移除指定的key,對應的value同時消失,void無回傳 |
m.count(元素型別1) | O(log n) | 回傳m內key為指定元素的數量,因key不重複故結果只有0、1,回傳int |
m.clear() | O(n) | 清空m中的所有元素,void無回傳,複雜度之n為原本m的大小 |
m.size() | O(1) | 回傳m內的元素數量,回傳int |
m.empty() | O(1) | 回傳m是否為空,回傳bool |
#include<bits/stdc++.h>
using namespace std;
int main(){
map<string,int> m;
m["Samuel"]=5;//若直接使用不存在map內的key,會自動新增進去並賦值,等同m.insert({"Samuel",5});
cout<<m["Samuel"]<<'\n';//輸出「5」
m["Alice"]=10;
m.insert({"Bob",15});
cout<<"m has "<<m.size()<<" elements\n";//輸出「m has 3 elements」
for(auto p:m)cout<<p.first<<','<<p.second<<' ';//輸出「Alice,10 Bob,15 Samuel,5」
cout<<'\n';
//需要注意若存取了未出現的key會自動新增一項,建議若有key不在map內的可能,先用m.count()來確認是否存在
cout<<m["Allen"]<<'\n';//輸出「0」,因為該key不存在且並未直接賦值value,會自動設為0
cout<<"m has "<<m.size()<<" elements\n";//輸出「m has 4 elements」
for(auto p:m)cout<<p.first<<','<<p.second<<' ';//輸出「Alice,10 Allen,0 Bob,15 Samuel,5」
}
在用for(auto p:m)遍歷map m時,auto會依據m的兩個型別自動判斷成對應的pair,另外該pair的first為key、與set的元素同概念,故不能修改,但second為value可以修改,故可以寫for(auto &p:m)此時p.second為m內value的別名,可以對m之內容進行修改,但需要注意p.first仍然無法修改。
以下介紹map的迭代器,大致上與set迭代器的用法相同,只不過取出的值是pair<型別1,型別2>,此時用*運算時需要加上(),如「(*it).second」,否則寫it.second會先取it的成員second,但it無此成員會導致編譯失敗。
上面提到若需取值再取成員,需要把取值的部分()起來先做,但這樣並不好寫,因此C++有->運算,我習慣稱其箭頭,可以做到對指標或迭代器取其值的成員,如「it->second」等同「(*it).second」
用法 | 複雜度 | 功能說明 |
---|---|---|
m.begin() | O(1) | 回傳m起始位置也就是key最小元素的迭代器 |
m.end() | O(1) | 回傳m結束位置的迭代器,此處實際上沒有儲存資料,m之key最大元素在prev(m.end()) |
m.find(元素型別1) | O(log n) | 回傳key為指定值的元素的迭代器,若沒有該key不在m裡則回傳m.end() |
m.lower_bound(元素型別1) | O(log n) | 回傳在m中,順序在指定key後(或等於指定key)的元素的迭代器 |
m.upper_bound(元素型別1) | O(log n) | 回傳在m中,順序在指定key後(不等於指定key)的元素的迭代器 |
若有回覆需求可以選擇提供email。