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(数组)
定长数组。和 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(双端队列/双端数组)
咕咕咕
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()