STL 容器用法简要整理(未完成)

STL 容器用法简要整理

本文将简要介绍 C++14 中可以使用的 STL 容器的用法。根据 CCF 规定,这些容器都可以在比赛中使用。

本文中的代码均为 C++14。

本文中的代码均已引入了相关的库,并 using namespace std

共同点

  • 特性:所有能用下标访问的 STL 容器,下标都是从 0 开始,到 size - 1 结束,如 vectorarraystring

  • 初始化:STL 容器的初始化都是形如 container<T> name 的形式,其中 T 是数据类型,如 intchar,结构体或其它 STL 容器;name 是你起的容器的名字。例如,创建一个空的 intvector 可以这么写: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 的值。时间复杂度常数。但是有一个例外:arrayswap() 是线性的!(参见此处。作为对比,vectormap 的时间复杂度都是常数(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):迭代器类似指针。用 * 可以解引用。

    声明时可以用 autoauto it = v1.begin(),也可以用 container::iteratorvector<int>::iterator it

    begin() 返回指向容器开头的迭代器,end() 返回指向容器最后一个元素的后继的迭代器。(所以实际上 end() 不指向任何一个元素,元素的地址范围是 [begin(), end())。)

    对于 vectorarray 等可以用下标访问的容器,可以用 *(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 共有的成员函数不再列出):

  • 访问元素的方式:

    1. 通过 [] 访问。
    2. 通过 at() 访问。与前者的区别是用 at 访问时会检测有没有越界。at() 的效率低于 [],所以一般情况下都用 [],但是如果觉得自己可能会 RE,可以用 at() 以便于调试。
    3. front() 访问首元素,back() 访问尾元素。(注意与 begin()end() 区分,它们是迭代器。)
    4. 用迭代器访问:*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

    时间复杂度和 nvector 原来大小的差线性相关。

    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()。用法:

    1. assign(n, val):把 vector 替换nval。时间复杂度 \(O(n)\)vector 的大小多退少补。“替换”的意思是会覆盖原有的元素。
    2. 咕咕咕
  • inserterase():插入删除操作。通常情况下不使用这个函数,因为时间复杂度是线性。如果要做到常数的插入和删除,应该使用链表。这里不做介绍。

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()

posted @ 2024-10-11 21:24  DengStar  阅读(28)  评论(0编辑  收藏  举报