STL 容器用法簡要整理
本文將簡要介紹 C++14 中可以使用的 STL 容器的用法。根據 CCF 規定,這些容器都可以在比賽中使用。
本文中的程式碼均為 C++14。
本文中的程式碼均已引入了相關的庫,並 using namespace std
。
共同點
-
特性:所有能用下標訪問的 STL 容器,下標都是從
0
開始,到size - 1
結束,如vector
,array
,string
。 -
初始化:STL 容器的初始化都是形如
container<T> name
的形式,其中T
是資料型別,如int
,char
,結構體或其它STL
容器;name
是你起的容器的名字。例如,建立一個空的int
型vector
可以這麼寫:vector<int> v
。 -
=
:賦值運算子。可以把某個容器的值賦值給另一個同類容器。以vector
為例:int main() { vector<int> v1{1, 2, 3, 4, 5}; vector<int> v2 = v1; // 兩個容器的型別必須相同。vector<int> 和 vector<long long> 也是不能互相賦值的。 for(int x: v2) cout << x << ' '; cout << endl; return 0; }
輸出:
1 2 3 4 5
。 -
size()
:返回容器內的元素個數。(如無特別說明,提到的函式都是成員函式。) -
empty()
:bool
型,返回容器是否為空。 -
swap()
:a.swap(b)
表示交換容器a
和容器b
的值。時間複雜度常數。但是有一個例外:array
的swap()
是線性的!(參見此處。作為對比,vector
和 map 的時間複雜度都是常數(constant)。)int main() { vector<int> v1{1, 2, 3, 4, 5}; vector<int> v2{6, 7, 8, 9, 10}; v1.swap(v2); cout << "v1: "; for(int x: v1) cout << x << ' '; cout << endl; cout << "v2: "; for(int x: v2) cout << x << ' '; cout << endl; return 0; }
輸出:
v1: 6 7 8 9 10 v2: 1 2 3 4 5
-
>
/>=
/<
/<=
/==
/!=
:按字典序比較兩個容器。無序容器(如unordered_map
)只支援==
和!=
。int main() { vector<int> v1{1, 2, 3, 4, 5}, v2{1, 2, 4, 2, 1}, v3{1, 2, 3, 4, 5, 6}; cout << "v1 < v2: " << boolalpha << (v1 < v2) << endl; cout << "v1 < v3: " << boolalpha << (v1 < v3) << endl; string s1("cat"), s2("cat"), s3("cap"); cout << "s1 == s2: " << boolalpha << (s1 == s2) << endl; cout << "s1 < s3: " << boolalpha << (s1 < s3) << endl; return 0; }
輸出:
v1 < v2: true v1 < v3: true s1 == s2: true s1 < s3: false
-
迭代器(iterator):迭代器類似指標。用
*
可以解引用。宣告時可以用
auto
:auto it = v1.begin()
,也可以用container::iterator
:vector<int>::iterator it
。begin()
返回指向容器開頭的迭代器,end()
返回指向容器最後一個元素的後繼的迭代器。(所以實際上end()
不指向任何一個元素,元素的地址範圍是[begin(), end())
。)對於
vector
,array
等可以用下標訪問的容器,可以用*(it + c)
的形式來訪問元素,其中it
是迭代器,c
是整數。也可以用這種形式修改元素,就像指標一樣。這裡以array
為例:int main() { array<int, 5> a{1, 2, 3, 4, 5}; auto it = a.begin(), it2 = a.end(); cout << *it << ' ' << *(it + 1) << ' ' << *(it2 - 1) << endl; *it = 2, *(it + 1) = 10; for(int x: a) cout << x << ' '; cout << endl; return 0; }
輸出:
1 2 5 2 10 3 4 5
更多關於迭代器的知識,參見此處。
vector(向量)
std::vector - cppreference.com
可變長度陣列。解決了 C 風格陣列長度只能是常數的問題。它支援如下操作:\(O(1)\) 的隨機訪問,\(O(n)\) 的插入/刪除元素。
我們經常能看到這樣的題目(尤其是在 CF 上):有一個 \(n \times m\) 的矩陣,我們要儲存每個元素的資訊,而 \(nm \le 10^6\),但對於 \(n\),\(m\) 的大小沒有單獨限定。這時候用陣列就不能保證既不爆空間,又能存下所有資料。vector
的作用就體現出來了。
注意,不要使用 vector<bool>
。vector<bool>
不是 vector
,使用它可能會出現意外的錯誤。如必須使用,可以用 vector<char>
代替,二者空間相同。
需要注意的是,vector
的常數有時劣於陣列(待驗證)。
-
初始化:
vector
的初始化方式有許多種,使用合理的方式可以為我們提供便利。參見此處。下面是 5 種常用的初始化方法:
#define vec_print(x) cout << #x": ", print(x) void print(vector<int> &v) { for(int x: v) cout << x << ' '; cout << endl; } int main() { vector<int> v1; // 1. 建立空 vector,時間複雜度常數 int n = 5; vector<int> v2(n); // 2. 建立長度為n的vector,時間複雜度線性。 // 元素的初值都為0。下面會討論這個初值是怎麼得來的。 // 這體現了與陣列的不同之處:長度可以是變數。 vector<int> v3(n, 42); // 3. 建立長度為n的vector,每個元素都是42。時間複雜度線性。 vector<int> v4(v3); // 4. 建立一個與v3相同的vector。時間複雜度與v3的大小線性相關。 vector<int> v5(v4.begin() + 1, v4.end() - 1); // 5. 把[v4[1] ~ v4[4])中的元素複製到v5中(注意是左閉右開區間!) // 時間複雜度和複製的元素數量線性相關。 vector<int> v6{1, 2, 3, 4, 5}; // 6. 用列表初始化,時間複雜度? // (我不知道這個方法的時間複雜度,但是元素太多的時候一般不會用列表初始化,因此時間可以忽略不計) vec_print(v1), vec_print(v2), vec_print(v3), vec_print(v4), vec_print(v5), vec_print(v6); return 0; }
輸出:
v1: v2: 0 0 0 0 0 v3: 42 42 42 42 42 v4: 42 42 42 42 42 v5: 42 42 42 v6: 1 2 3 4 5
關於方法 2 中,元素的初值:
好吧,我沒有完全搞懂。cppreference 上的原文表示元素的值是
default-inserted instance of T
,但我不知道什麼叫 "default-inserted"。我猜測應該是元素預設的值:例如整型預設是0
。如果元素型別是結構體,而結構體有建構函式,那麼元素的初值透過建構函式得來:
struct Node { int x; Node(): x(42) {} }; int main() { vector<Node> v(5); for(auto nd: v) cout << nd.x << ' '; cout << endl; return 0; }
輸出:
42 42 42 42 42
關於方法 5:
用某個
vector
中一段元素的值初始化另一個vector
,兩個vector
的元素型別可以不相同,但要滿足可以互相轉換。例如int
可以轉成long long
。或者,如果被初始化的vector
的元素是結構體,要有對應的建構函式。需要注意的是,如果兩個
vector
的元素型別不同,就不能用vector<int> v2(v1)
之類的形式來初始化,即使兩種元素型別可以互相轉換或者有建構函式也不行。struct Node { string str; Node(int x): str(to_string(x * 10) + "str") {} }; int main() { vector<int> v1{1, 2, 3, 4, 5}; vector<long long> v2(v1.begin(), v1.end()); // ok,int 和 long long 可以轉換 // vector<string> v3(v1.begin(), v1.end()); // wrong,int 不能轉成 long long vector<Node> v4(v1.begin(), v1.end()); // ok,存在 int 到 Node 的建構函式 // vector<Node> v5(v1); // wrong,不同元素型別的 vector 不能互相初始化 cout << "v1: "; for(auto x: v1) cout << x << ' '; cout << endl; cout << "v2: "; for(auto x: v2) cout << x << ' '; cout << endl; cout << "v4: "; for(auto x: v4) cout << x.str << ' '; cout << endl; return 0; }
輸出:
v1: 1 2 3 4 5 v2: 1 2 3 4 5 v4: 10str 20str 30str 40str 50str
以下是一些常用的成員函式(STL 共有的成員函式不再列出):
-
訪問元素的方式:
- 透過
[]
訪問。 - 透過
at()
訪問。與前者的區別是用at
訪問時會檢測有沒有越界。at()
的效率低於[]
,所以一般情況下都用[]
,但是如果覺得自己可能會RE
,可以用at()
以便於除錯。 front()
訪問首元素,back()
訪問尾元素。(注意與begin()
和end()
區分,它們是迭代器。)- 用迭代器訪問:
*it
表示it
指向的元素的值,例如*begin()
就表示首元素的值。
int main() { vector<int> v{1, 2, 3, 4, 5}; cout << v.front() << ' ' << v[1] << ' ' << v.at(2) << ' ' << *(v.begin() + 3) << ' ' << v.back() << endl; return 0; }
輸出:
1 2 3 4 5
用
at()
訪問時,如果越界,會輸出錯誤資訊。int main() { vector<int> v{1, 2, 3, 4, 5}; cout << v.at(5) << endl; return 0; }
輸出:
terminate called after throwing an instance of 'std::out_of_range' what(): vector::_M_range_check: __n (which is 5) >= this->size() (which is 5)
- 透過
-
新增元素:
push_back()
:向vector
的末尾增加元素。這麼做會增加vector
的長度。注意我們沒有push_front()
,要實現類似操作,得用deque
。 -
刪除元素:
pop_back()
。同理,沒有pop_front()
。 -
改變
vector
的大小:resize()
。resize(n)
表示把大小改為n
。如果原先的長度大於n
,會刪除多餘的元素;否則:- 如果呼叫
resize(n)
,會在末尾新增元素的預設值; - 如果呼叫
resize(n, val)
,會在末尾補上val
。
時間複雜度和
n
與vector
原來大小的差線性相關。int main() { vector<int> v{1, 2, 3, 4, 5}; v.resize(3); for(int x: v) cout << x << ' '; cout << endl; v.resize(10, 42); for(int x: v) cout << x << ' '; cout << endl; cout << "v.size() is " << v.size() << endl; return 0; }
輸出:
1 2 3 1 2 3 42 42 42 42 42 42 42 v.size() is 10
- 如果呼叫
-
assign()
:給vector
填充某個元素,有點像fill()
。用法:assign(n, val)
:把vector
替換為n
個val
。時間複雜度 \(O(n)\)。vector
的大小多退少補。“替換”的意思是會覆蓋原有的元素。- 咕咕咕
-
insert
,erase()
:插入刪除操作。通常情況下不使用這個函式,因為時間複雜度是線性。如果要做到常數的插入和刪除,應該使用連結串列。這裡不做介紹。
array(陣列)
std::array - cppreference.com
定長陣列。和 C 中的陣列一樣,長度必須為常數,效率優於 vector
,和 C 中的陣列相當。個人建議用 array
代替所有的定長陣列,用 vector
代替所有的不定長陣列。
大部分使用方法與 vector
相同,除了不能加入/刪除末尾元素(push_back()
/pop_back()
),當然也不能 insert()
和 erase()
。
下面是一些(個?)特有的函式:
-
fill()
:fill(x)
表示把array
全部填充為x
。時間複雜度與array
大小線性相關。int main() { array<char, 5> a{'a', 'a', 'a', 'a', 'a'}; a.fill('b'); for(char ch: a) cout << ch; cout << endl; return 0; }
輸出:
bbbbb
(覆蓋了原先的值)
deque(雙端佇列/雙端陣列)
std::deque - cppreference.com
咕咕咕
string(字串)
(嚴格來說 string
不算 container
,但它很重要,這裡一併整理用法。)
在 C 中,字串是透過 char
陣列實現的。而在 C++ 中,我們終於有了原生的字串型別——string
。它自帶許多高效的函式,可以為我們寫題(特別是字串相關的模擬題)帶來極大便利。與此同時,它的常數也十分優秀。
-
初始化:
int main() { string s1; // 1. 建立空字串 string s2("test"); // 2. 建立特定字串 int n = 5; string s3(n, '='); // 3. 建立含n個'='的字串,時間複雜度線性 auto print = [&](string name, string str) {cout << name << ": " << str << endl;}; print("s1", s1), print("s2", s2), print("s3", s3); return 0; }
輸出:
s1: s2: test s3: =====
-
find()
:查詢某個子串/字元。如果存在該子串,返回第一個子串的第一個字元的下標;否則,返回string::npos
(本質上是一個size_t
型的數)。int main() { string str("This is a string."); /* ^ ^ ^ 2 5 15 */ cout << str.find("This") << ' ' << str.find("is") << endl; cout << str.find('g') << endl; // 也可以找 char cout << str.find("is", 3) << endl; // find(s, pos):從下標pos處尋找s cout << str.find("1234") << ' ' << boolalpha << (str.find("1234") == string::npos) << endl; // 找不到示例 return 0; }
輸出:
0 2 15 5 18446744073709551615 true
其中
18446744073709551615
就是string::npos
,具體的值由編譯器決定。 -
substr()