Content
STL
介绍
简介
(Standard Template Library)标准模板库
广义上分为三类:
- 容器(container)
- 算法(algorithm)
- 迭代器(iterator)
六大组件:
-
容器
一种类模板,有各种数据结构。
-
算法
一种函数模板,有各种常用算法。
-
迭代器
一种类模板,迭代器是容器与算法之间的胶合剂。
-
仿函数(伪函数)
一种类模板,用来协助算法完成不同的策略。
-
适配器(配接器)
一种用来修饰容器或者仿函数或迭代器接口的东西。
-
空间配置器
一种类模板,负责空间的配置和管理。
容器
两类:
-
序列式容器
如 Vector、Deque、List 等,序列式容器强调值的顺序,每个元素都有物理意义上固定的系列关系。
-
关联式容器
如 Set/multiset、Map/multimap 等,关联式容器是非线性的容器,元素之间没有顺序关系,例如树结构。
JSON 就是关联式容器。
算法
两类:
-
质变算法
质变算法是运算过程会改变元素内容的算法,例如拷贝,替换,删除。
-
非质变算法
非质变算法是运算过程不会改变元素内容的算法,例如查找、计数、遍历。
迭代器
迭代器是依顺序访问某个容器所含的各个元素的方法。
种类:
-
输入迭代器
提供对数据的只读访问,支持 ++、==、!=
-
输出迭代器
提供只写访问,支持 ++
-
前向迭代器
提供读写访问,并且可以向前推进迭代器,支持 ++、==、!=
-
双向迭代器(使用较多)
提供读写访问,并且可以前后推进迭代器,支持 ++、==
-
随机访问迭代器(使用较多)
提供读写访问,并可以跳跃方式访问容器的任意数据,功能最强,
支持 ++、–、[n]、-n、<、<=、>、>=
针对实现来说,有几种类型的迭代器:
- iterator 普通迭代器
- reverse_iterator 逆序迭代器
- const_iterator 只读迭代器
常用容器
string 容器
-
assign 方法
string& assign(const char* s,int n); // 将字符串 s 的前 n 个字符赋值给当前字符串 string& assign(const strubg& s,int start, int n); // 将字符串 s 从 start 开始的 n 个字符赋值给当前字符串,n 从 0 开始
-
字符串存取
char& operator[](int n); // 访问越界就崩了 char& at(int n); // 访问越界会抛出异常
-
字符串查找
正找:
int find(const string& str, int pos = 0) const; // 从 pos 开始查找,返回 str 在当前字符串中第一次出现的位置。 int find(const string& str, int pos, int n) const; // 从 pos 开始查找,返回 str 的前 n 个字符在当前字符串中第一次出现的位置。
倒找:
int rfind(const string& str, int pos = npos) const; // 从 pos 开始查找,返回 str 在当前字符串中最后一次出现的位置。 int rfind(const string& str, int pos, int n) const; // 从 pos 开始查找,返回 str 的前 n 个字符在当前字符串中最后一次出现的位置。
找不到返回 -1
-
字符串替换
string& replace(int pos, int n, const string& str); // 替换从 pos 开始的 n 个字符为字符串 str
-
字符串截取(子串)
string substr(int pos = 0, int n = npos) const; // 返回从 pos 开始的 n 个字符组成的字符串
应用:
获取两文本中间
string getMiddleStr(string source,string left, string right) { return source.substr(source.find(left), source.find(right) - source.find(left)); }
-
字符串插入和删除
string& insert(int pos, const string& s); // 将 s 插入到当前字符串的 pos 位置后 string& erase(int pos, int n = npos); // 删除从 pos 开始的 n 个字符
-
C-style 字符串转换
const char* p = str.c_str();
vector 容器
(单端数组、动态数组)
vector 内部维护一段线性空间,当空间不足时会额外开辟一段连续的更大的空间,然后进行数据的拷贝,最后丢弃原空间。
他被称为单端数组,是因为其一端封闭,数据仅在另一端成长,这导致其头插数据的开销非常大,这个问题可由 deque 容器解决,deque 容器是双端数组,而且其拥有更小的扩容开销。
-
头文件
#include <vector>
-
获得一个容器(构造函数)
vector<type> v; // 默认构造
vector<type> v(type* begin, type* end); // 将 begin 到 end 指向的内存范围拷贝给自身
注意!end 指向的是尾元素的下一个位置。
vector<type> v(size_t n, type elem); // 将 n 个 elem 拷贝给自身
-
数据操作
-
拷贝
void assign(type* begin, type* end); // 将 begin 到 end 指向的内存范围拷贝给自身
void assign(size_t n, type elem); // 将 n 个 elem 拷贝给自身
-
交换
void swap(vector& v2); // 互换两容器的内容
swap 的应用:收缩内存
vector 在缩小长度的时候并不会缩小容量(这个结论后面会通过代码检验),那么饿我们想要收缩容量,可以使用 swap。
vector<int>(oldV).swap(oldV);
resize 后,尾指针将移动至新范围的尾部,此时通过拷贝构造实例化的新对象便仅拷贝新范围内的数据,然后通过 swap 来交换容器的指针,就拿到收缩了内存的容器。
此后匿名对象会自行销毁。
-
是否为空
bool empty();
-
获取容量
size_t capacity()
-
获取大小
size_t size()
-
改变大小
void resize(size_t newSize, type val); // 若容器变大,则多余位置由 val 填充 void resize(size_t newSize); // 若容器变大,则多余位置由 0 填充
-
预留内存
void reserve(const size_t newcapacity);
-
数据存取
直接访问:
type& operator[](size_t n); // 访问越界就崩了 type& at(size_t n); // 访问越界会抛出异常
获取首尾元素:
type& front(); // 返回容器的首个元素的引用 type& back(); // 返回容器的尾元素的引用
尾插尾删:
void push_back(type& ele); void pop_back();
数据插入:
iterator insert(iterator where, size_t count, type val); // 从 where 处前插 count 个值 val iterator insert(iterator where, type val); // 从 where 处前插 1 个值 val iterator insert(iterator where, iterator first, iterator last); // 将 first 到 last 范围内的数据插入至 where
数据删除:
iterator erase(iterator first, iterator last); // 删除迭代器从 first 到 last 之间的元素 iterator erase(iterator where); // 删除迭代器指向的元素 void clear(); //删除容器中所有元素
-
-
迭代器
-
先拿到迭代器
// 首指针 vector<int>::iterator itBegin = v.begin(); // 尾指针(最后一个元素的下一个位置) vector<int>::iterator itEnd = v.end();
-
方法一
vector<int>::iterator itBegin = v.begin(); vector<int>::iterator itEnd = v.end(); while (itBegin != itEnd) { cout << *itBegin++ << endl; }
-
方法二
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) { clog << *it << endl; }
-
方法三:算法
#include <algorithm> // ... void callback(int value) { cout << value; } // ... for_each(v.begin(), v.end(), callback);
-
方法四:
for (auto& ele : v) { cout << ele << endl; }
这是基于范围的 for 循环,C++11 的语句。
另:
为了能跟写 python 和 js 一样爽,我实现了一个 range 类。
class range { private: long* startp; long* endp; public: long* begin() { return this->startp; } long* end() { return this->endp; } range(long min, long max) { if (min > max) { long temp = min; min = max; max = temp; } this->startp = new long[max - min + 2]; this->endp = startp + max - min + 1; for (long ele = min; ele <= max; ele++) { startp[ele - min] = ele; } } ~range() { delete[] startp; } };
当我们想要循环指定次数时再也不用写一长串了,只需要简单的:
for (auto& ele : range(10, 20)) { cout << ele << endl; }
-
逆序遍历:
拿到迭代器
rbegin()
、rend()
// 首指针 vector<int>::reverse_iterator itBegin = v.rbegin(); // 尾指针(最后一个元素的下一个位置) vector<int>::reverse_iterator itEnd = v.rend();
迭代方法不变,例如:
vector<int>::reverse_iterator itBegin = v.rbegin(); vector<int>::reverse_iterator itEnd = v.rend(); while (itBegin != itEnd) { cout << *itBegin++ << endl; }
-
随机访问迭代器
vector 的迭代器是随机访问迭代器,要判断某迭代器是否是随机访问迭代器,可用下述代码测试:
iterator it = foo.begin(); it = it + 3;
编译通过说明是随机访问迭代器,不通过说明不是。
-
-
vector 的空间分配策略:
vector 维护的是一段线性空间,当空间不足时会开辟一段连续的更大的空间,然后进行数据的拷贝。
扩展空间时,扩展量大致是当前空间的 1.5 倍。
不会缩小空间。
测试代码:
#include <iostream> #include <vector> using namespace std; int main() { vector<int> v; int oldCapacity = 0; // 添加数据 for (auto& val : range(1, 100000)) { v.push_back(val); if (oldCapacity != v.capacity()) { cout << v.capacity() << endl; } oldCapacity = v.capacity(); } cout << endl; // 删除数据 for (auto& val : range(1, 100000)) { v.pop_back(); if (oldCapacity != v.capacity()) { cout << v.capacity() << endl; } oldCapacity = v.capacity(); } system("pause"); return EXIT_SUCCESS; }
输出:
1
2
3
4
6
9
13
19
28
42
63
94
141
211
316
474
711
1066
1599
2398
3597
5395
8092
12138
18207
27310
40965
61447
92170
138255请按任意键继续. . .
-
注意!
vector 分配空间的策略如此,当容器扩展后,所有迭代器将失效。
deque 容器
(双端数组、没有容量)
deque 是双端数组,其成长方向有两个,这使得其前后插入数据的开销是一个常数,解决了 vector 头插开销令人难以接受的问题。
deque 容器的扩容也与 vector 非常不同,vector 是不断地申请新空间,不断地对数据进行拷贝迁移。
而 deque 则是将新申请到的空间串接在一端,其内部维护着这些分段空间,维持着整体连续的假象。这也造成了其代码实现要比 vector 或 list 多得多,迭代器的架构也非常复杂。
-
头文件
#include <deque>
-
获得一个容器(构造函数)
deque<type> v; // 默认构造
deque<type> v(type* begin, type* end); // 将 begin 到 end 指向的内存范围拷贝给自身
注意!end 指向的是尾元素的下一个位置。
deque<type> v(size_t n, type elem); // 将 n 个 elem 拷贝给自身
-
数据操作
-
拷贝
void assign(type* begin, type* end); // 将 begin 到 end 指向的内存范围拷贝给自身
void assign(size_t n, type elem); // 将 n 个 elem 拷贝给自身
-
交换
void swap(deque& v2); // 互换两容器的内容
-
是否为空
bool empty();
-
获取大小
size_t size()
-
改变大小
void resize(size_t newSize, type val); // 若容器变大,则多余位置由 val 填充 void resize(size_t newSize); // 若容器变大,则多余位置由 0 填充
-
数据存取
直接访问:
type& operator[](size_t n); // 访问越界就崩了 type& at(size_t n); // 访问越界会抛出异常
获取首尾元素:
type& front(); // 返回容器的首个元素的引用 type& back(); // 返回容器的尾元素的引用
双端插入:
void push_back(type& ele); void push_front(type& ele); void pop_back(); void pop_front();
数据插入:
iterator insert(iterator where, size_t count, type val); // 从 where 处前插 count 个值 val iterator insert(iterator where, type val); // 从 where 处前插 1 个值 val iterator insert(iterator where, iterator first, iterator last); // 将 first 到 last 范围内的数据插入至 where
数据删除:
iterator erase(iterator first, iterator last); // 删除迭代器从 first 到 last 之间的元素 iterator erase(iterator where); // 删除迭代器指向的元素 void clear(); // 删除容器中所有元素
-
-
迭代器
使用方法与 vector 相同,均为随机访问迭代器。
stack 容器
stack(栈),是一个栈结构的容器,不提供迭代器,也不提供遍历的方法。
stack 容器允许新增元素,移除元素,取得栈顶元素。
-
头文件
#include <stack>
-
获得一个容器(构造函数)
stack<type> v; // 默认构造
-
数据操作
-
是否为空
bool empty();
-
获取大小
size_t size()
-
数据存取
void push(type& elem); // 压栈 void pop(); // 弹栈 type& top(); // 返回栈顶元素的引用
-
-
迭代器
stack 没有迭代器
queue 容器
queue (队列),是一个队列结构的容器,不提供迭代器,也不提供遍历的方法。
queue 容器允许从一端新增元素,从另一端移除元素。
-
头文件
#include <queue>
-
获得一个容器(构造函数)
queue<type> q; // 默认构造
-
数据操作
-
是否为空
bool empty();
-
获取大小
size_t size()
-
数据存取
void push(type& elem); // 往队尾添加元素 void pop(); // 从队头移除第一个元素 type& back(); // 返回最后一个元素 type& front(); // 返回第一个元素
-
-
迭代器
queue 没有迭代器
list 容器
(双向循环链表)
list 容器是双向循环链表,插入和移除数据的开销是常数。
它提供的迭代器是双向迭代器,而不是随机访问迭代器。由于链表的结构特殊性,指向有效数据的迭代器永远不会失效。
-
头文件
#include <list>
-
获得一个容器(构造函数)
list<type> l; // 默认构造
list<type> l(type* begin, type* end); // 将 begin 到 end 指向的内存范围拷贝给自身
注意!end 指向的是尾元素的下一个位置。
list<type> l(size_t n, type elem); // 将 n 个 elem 拷贝给自身
-
数据操作
-
拷贝
void assign(type* begin, type* end); // 将 begin 到 end 指向的内存范围拷贝给自身
void assign(size_t n, type elem); // 将 n 个 elem 拷贝给自身
-
交换
void swap(list& l); // 互换两容器的内容
-
是否为空
bool empty();
-
获取大小
size_t size()
-
改变大小
void resize(size_t newSize, type val); // 若容器变大,则多余位置由 val 填充 void resize(size_t newSize); // 若容器变大,则多余位置由 0 填充
-
数据存取
获取首尾元素:
type& front(); // 返回容器的首个元素的引用 type& back(); // 返回容器的尾元素的引用
双端插入:
void push_back(type& ele); void push_front(type& ele); void pop_back(); void pop_front();
数据插入:
iterator insert(iterator where, size_t count, type val); // 从 where 处前插 count 个值 val iterator insert(iterator where, type val); // 从 where 处前插 1 个值 val iterator insert(iterator where, iterator first, iterator last); // 将 first 到 last 范围内的数据插入至 where
数据删除:
iterator erase(iterator first, iterator last); // 删除迭代器从 first 到 last 之间的元素 iterator erase(iterator where); // 删除迭代器指向的元素 size_t remove(type& elem); // 删除容器中所有与 elem 值匹配的元素。 // 注意!对于自定义类型,需要重载 == 号 bool operator==(const Type& elem); void clear(); // 删除容器中所有元素
链表操作:
void reverse(); // 反转链表 void sort(); // 排序,从小到大 void sort(funType* callback); // 排序,通过回调函数来排序
注意!所有不提供随机访问迭代器的容器都不允许使用 STL 提供的算法。
-
-
迭代器
同期提供双向迭代器
set/multiset 容器
(关联式容器)
set 容器
set 容器的特点是,所有元素都会根据元素的键值自动排序,且不允许出现重复的键值。(可以插入重复的键值,但没用)
虽然提到键值,但其不像 map 容器一样是键值对,它的元素即是键也是值。
它的迭代器是 const_iterator,不允许修改,因为其元素关系到组织排序。
其指向有效数据的迭代器永远不会失效。
multiset 容器
multiset 容器的特点和用法与 set 容器几乎完全相同,唯一的不同点在于它允许键值重复,而 set 不允许。
两容器的底层结构是红黑树(一种平衡二叉树)。
-
头文件
#include <set>
-
获得一个容器(构造函数)
set<type> s; multiset<type> ms; // 默认构造
-
数据操作
-
交换
void swap(list& l); // 互换两容器的内容
-
是否为空
bool empty();
-
获取大小
size_t size()
-
改变大小
void resize(size_t newSize, type val); // 若容器变大,则多余位置由 val 填充 void resize(size_t newSize); // 若容器变大,则多余位置由 0 填充
-
数据存取
-
对祖:
对组是库定义的一个模板类的实例,其中储存有两个数据,用于同时返回两个数据。
类型为:
pair<type1, type2>
对于一个对祖,可以通过以下属性来获取其中存储的数据:
p.first; p.second;
分别对应 type1 和 type2。
创建对祖:
pair<int, string> p(10, "string");
或者:
pair<int, string> p = make_pair(10, "string");
-
数据插入:
pair<iterator, bool> insert(type elem); // 向容器中插入一个值
-
数据删除:
iterator erase(iterator first, iterator last); // 删除迭代器从 first 到 last 之间的元素 iterator erase(iterator where); // 删除迭代器指向的元素 size_t erase(type elem); // 删除容器中值为 elem 的元素 void clear(); // 删除容器中所有元素
-
set 查找操作:
iterator find(type& key); // 查找 key 是否存在,返回迭代器,未找到范围 set.end() size_t count(type& key); // 返回 key 的元素个数 iterator lower_bound(type& key); // 本意是返回第一个小于等于 key 的迭代器,但实际上结果与 find 相同 iterator upper_bound(type& key); // 返回第一个大于 key 的迭代器。 pair<iterator, iterator> equal_range(type& key); // 返回一个 pair,包含 lower_bound 和 upper_bound 的返回值 // pair 类型为 pair<set<type>::iterator, set<type>::iterator> p; // 可以通过 p 拿到两个迭代器: p.first, p.second;
注意!所有不提供随机访问迭代器的容器都不允许使用 STL 提供的算法。
-
指定容器的插入规则:
容器默认插入顺序是从小到大,如果想要自己指定插入顺序,则需要在实例化容器时额外**提供一个模板参数**。
这个类型实参里需要提供一个重载的方法,实现一个仿函数,提供排序规则。
例如:
class rule { public: bool operator()(const type& a, const type* b) const { return a > b; } };
实例化容器时:
set<type, rule> s;
所以,对于自定义数据类型,需要提供一个 < 号常函数重载(因为 < 是默认排序)。
bool operator<(const type& obj) const { return /* ... */ ; }
或者提供一个排序规则。
set<type, rule> s;
-
-
-
迭代器
容器提供双向迭代器。
map/multimap 容器
(关联式容器)
map 与 JSON 的抽象结构相似,是键值对。插入时会根据键值排序。
元素的类型是刚才提到的 pair<T1, T2>
。第一个是键,第二个是值。
与 set/multiset 容器相同,multimap 容器允许出现相同的键值,且其底层实现也是红黑树。
-
头文件
#include <map>
-
获得一个容器(构造函数)
map<type1, type2> m; // 默认构造
-
数据操作
-
交换
void swap(map& m); // 互换两容器的内容
-
是否为空
bool empty();
-
获取大小
size_t size()
-
数据存取
-
直接访问:
type& operator[](size_t n); // 访问越界就崩了 type& at(size_t n); // 访问越界会抛出异常
-
数据插入:
pair<iterator, bool> insert(pair<type1, type2> p);
示例:
// 1.pair m.insert(pair<int, string>(3, "string")); // 2.make_pair m.insert(make_pair(3, "string")); // 3.(一般不这样用) m.insert(map<int, string>::value_type(3, "string")); // 4.(伪数组) 注意! multimap 不允许这种操作 m[1] = "string"; // 这种方法存在一个问题 // 当 m[1] 不存在时 m[1]; /* 等价于 */ m[1] = NULL; // 对于某些类型来说,就等于 type(NULL);
-
数据删除:
iterator erase(iterator first, iterator last); // 删除迭代器从 first 到 last 之间的元素 iterator erase(iterator where); // 删除迭代器指向的元素 iterator erase(type1& key); // 删除容器中键为 key 的元素 void clear(); // 删除容器中所有元素
-
查找操作:
iterator find(type1& key); // 查找 key 是否存在,返回迭代器,未找到范围 set.end() size_t count(type1& key); // 返回 key 的元素个数 iterator lower_bound(type1& key); // 本意是返回第一个小于等于 key 的迭代器,但实际上结果与 find 相同 iterator upper_bound(type1& key); // 返回第一个大于 key 的迭代器。 pair<iterator, iterator> equal_range(type1& key); // 返回一个 pair,包含 lower_bound 和 upper_bound 的返回值
-
指定容器的插入规则:
与 set/multiset 容器相同,实例化时额外提供一个伪函数的实现,就可以了。
map<type1, type2, rule> m;
-
-
-
迭代器
容器提供双向迭代器。
容器总结
迭代器类型
-
双向迭代器
map/multimap、set/multiset、list
-
随机访问迭代器
string、vector、deque、
-
不提供迭代器
stack、queue
底层实现
-
单端数组
vector
-
双端数组
deque
-
双向循环链表
list
-
二叉树
set/multiset、map/multimap
性能
vector | deque | list | set/multiset | map/multimap | |
---|---|---|---|---|---|
搜寻速度 | 慢 | 慢 | 特慢 | 快 | 快 |
迭代器 | 随机访问 | 随机访问 | 双向 | 双向 | 双向 |
优点 | 查找效率比较高 | 均衡的动态增删 + 数据访问 | 动态增删能力强 | 查找效率极高 | 查找效率极高 |
弱点 | 头部数据操作效率低 | 中间数据操作效率低 | 查找效率低 | 插入效率低 | 插入效率低 |
deque 有较好的动态增删能力,但牺牲了中间数据的操作效率。
list 有着效率非常高的动态增删能力,但牺牲了查找效率。
set 和 map 以插入效率为代价来维护底层的红黑树,从而增强了在大容量数据中的查找性能。
另外,在遍历 vector 时,使用 [] 效率很高,而遍历 deque 时则应使用迭代器。