首頁 關於 支持
上一篇:章節1-3 下一篇:章節2-1 章節1-4練習題
快速移動至特定內容
一般容器
關聯容器

章節1-4.STL(二)

一般容器

除了內建的演算法函式外,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
vector範例程式1

注意vector中取到索引值大於等於v.size()會導致程式掛掉,在v.size()為0時執行v.back()或v.pop_back()也會導致程式掛掉,因此執行這些函式時,若有可能發先v為空的情況請利用條件判斷進行處理。

要使用vector儲存輸入的陣列有兩個方式,一是每次輸入a後v.push_back(a),建議用此種方式即可,另一種是用以下補充的產生特定大小、初始值的vector來宣告。

補充:產生特定大小、初始值的vector

另外由於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());」,仔細想想就會發現其實都能對應起來。

vector範例程式2

看到這裡也許有的讀者會好奇迭代器還有什麼用途,其實迭代器很多時候是搭配前述lower_bound、upper_bound兩個二分搜尋函式使用,這點在後續練習題目時就會漸漸遇到。
如果對迭代器的操作、應用仍不熟悉也沒關係,可以先行閱讀後續內容,待實際需要使用到時再回來複習。

另外在vector的最後也要來介紹一種遍歷(全部跑過一遍稱為遍歷)vector的簡易寫法,算是有點偷懶的方法,並且不如用索引值操作來得有彈性,因此建議還是先熟悉利用索引值遍歷,此方法可以看看就好。

補充:vector遍歷簡易寫法

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
set範例程式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中,順序在指定元素後(不等於指定元素)的元素的迭代器
set範例程式2
觀念釐清:可以用set代替對資料sort嗎?

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中,順序在指定元素後(不等於指定元素)的元素的迭代器
multiset範例程式

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
map範例程式

在用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無此成員會導致編譯失敗。

補充:取值再取成員的快速寫法
用法複雜度功能說明
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)的元素的迭代器