STL 即标准模板库(Standard Template Library),是 C++ 标准库的一部分,里面包含了一些模板化的通用的数据结构和算法。STL 基于模版的实现,所以可以支持自定义的数据结构。html
STL 中一共有 6 大组件:node
参考资料:c++
仿函数 (Functor) 的本质就是在结构体中重载 ()
运算符。算法
例如:设计模式
struct Print { void operator()(const char *s) const { cout << s << endl; } }; int main() { Print p; p("hello"); }
这一律念将会在 priority_queue
中使用(在智能指针的 unique_ptr
自定义 deleter 也会用到)。数组
容器 (Container) 在 STL 中又分为序列式容器 (Sequence Containers) ,关联式容器 (Associative Containers) 和无序容器 (Unorderde Containers) .网络
常见的序列式容器包括有:vector, string, array, deque, list, forward_list
.数据结构
vector/stringless
底层实现:vector
是内存连续、自动扩容的数组,实质仍是定长数组。数据结构和算法
特色:
[]
运算符size == capacity
时,那么扩容为当前容量的 2 倍,并拷贝原来的数据==, !=, <, <=, >, >=
比较运算
<=>
运算符 (aka, three-way comparsion )。PS:string
的底层实现与 vector
是相似的,一样是内存连续、自动扩容的数组(但扩容策略不一样)。
array (C++11)
底层实现:array
是内存连续的 、 固定长度的数组,其本质是对原生数组的直接封装。
特色(主要是与 vector
比较):
[]
随机访问pop_front/back, erase, insert
这些操做。vector
的初始化方式为函数参数(如 vector<int> v(10, -1)
,长度可动态肯定),但 array
的长度须要在编译期肯定,如 array<int, 10> a = {1, 2, 3}
.须要注意的是,array
的 swap
方法复杂度是 \(\Theta(n)\) ,而其余 STL 容器的 swap
是 \(O(1)\),由于只须要交换一下指针。
deque
又称为“双端队列”。
底层实现:多个不连续的缓冲区,而缓冲区中的内存是连续的。而每一个缓冲区还会记录首指针和尾指针,用来标记有效数据的区间。当一个缓冲区填满以后便会在以前或者以后分配新的缓冲区来存储更多的数据。
特色:
[]
随机访问list
底层实现:双向链表。
特色:
[]
随机访问forwar_list (C++11)
底层实现:单向链表。
特色:
list
减小了空间开销[]
随机访问rbegin(), rend()
关联式容器包括:set/multiset
,map/multimap
。multi
表示键值可重复插入容器。
底层实现:红黑树。
特色:
自定义比较方式:
<
int
等内置类型,经过仿函数struct cmp { bool operator()(int a, int b) { return a > b; } }; set<int, cmp> s;
无序容器 (Unorderde Containers) 包括:unordered_set/unordered_multiset
,unordered_map/unordered_multimap
.
底层实现:哈希表。在标准库实现里,每一个元素的散列值是将值对一个质数取模获得的,
特色:
在实际应用场景下,假设咱们已知键值的具体分布状况,为了不大量的哈希冲突,咱们能够自定义哈希函数(仍是经过仿函数的形式)。
struct my_hash { size_t operator()(int x) const { return x; } }; unordered_map<int, int, my_hash> my_map; unordered_map<pair<int, int>, int, my_hash> my_pair_map;
四种操做的平均时间复杂度比较:
Containers | 底层结构 | 增 | 删 | 改 | 查 |
---|---|---|---|---|---|
vector/deque |
vector: 动态连续内存 deque: 连续内存+链表 |
\(O(n)\) | \(O(n)\) | \(O(1)\) | \(O(n)\) |
list |
双向链表 | \(O(1)\) | \(O(1)\) | \(O(1)\) | \(O(n)\) |
forward_list |
单向链表 | \(O(1)\) | \(O(n)\) | \(O(1)\) | \(O(n)\) |
array |
连续内存 | 不支持 | 不支持 | \(O(1)\) | \(O(n)\) |
set/map/multiset/multimap |
红黑树 | \(O(\log{n})\) | \(O(\log{n})\) | \(O(\log{n})\) | \(O(\log{n})\) |
unordered_{set,multiset} unordered_{map,multimap} |
哈希表 | \(O(1)\) | \(O(1)\) | \(O(1)\) | \(O(1)\) |
容器适配器 (Container Adapter) 其实并非容器(我的理解是对容器的一种封装),它们不具备容器的某些特色(如:有迭代器、有 clear()
函数……)。
常见的适配器:stack
,queue
,priority_queue
。
对于适配器而言,能够指定某一容器做为其底层的数据结构。
stack
deque
top, pop, push, size, empty
操做的时间复杂度均为 \(O(1)\) 。指定容器做为底层数据结构:
stack<TypeName, Container> s; // 使用 Container 做为底层容器
queue
deque
front, push, pop, size, empty
操做的时间复杂度均为 \(O(1)\) 。指定容器:
queue<int, vector<int>> q;
priority_queue
又称为 “优先队列” 。
vector
top, empty, size
push, pop
模版参数解析:
priority_queue<T, Container = vector<T>, Compare = less<T>> q; // 经过 Container 指定底层容器,默认为 vector // 经过 Compare 自定义比较函数,默认为 less,元素优先级大的在堆顶,即大顶堆 priority_queue<int, vector<int>, greater<int>> q; // 传入 greater<int> 那么将构造一个小顶堆 // 相似的,还有 greater_equal, less_equal
迭代器 (Iterator) 实际上也是 GOF 中的一种设计模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
迭代器的分类以下图所示。
STL 中各容器/适配器对应使用的迭代器以下表所示。
Container | Iterator |
---|---|
array | 随机访问迭代器 |
vector | 随机访问迭代器 |
deque | 随机访问迭代器 |
list | 双向迭代器 |
set / multiset | 双向迭代器 |
map / multimap | 双向迭代器 |
forward_list | 前向迭代器 |
unordered_{set, multiset} | 前向迭代器 |
unordered_{map, multimap} | 前向迭代器 |
stack | 不支持迭代器 |
queue | 不支持迭代器 |
priority_queue | 不支持迭代器 |
迭代器失效是由于向容器插入或者删除元素致使容器的空间变化或者说是次序发生了变化,使得原迭代器变得不可用。所以在对 STL 迭代器进行增删操做时,要格外注意迭代器是否失效。
网络上搜索「迭代器失效」,会发现不少这样的例子,在一个 vector
中去除全部的 2 和 3,故意用一下迭代器扫描(你们都知道能够用下标):
int main() { vector<int> v = {2, 3, 4, 6, 7, 8, 9, 3, 2, 2, 2, 2, 3, 3, 3, 4, 5, 6}; for (auto i = v.begin(); i != v.end(); i++) { if (*i==2 || *i==3) v.erase(i), i--; // correct code should be // if (*i==2 || *i==3) i=v.erase(i), i--; } for (auto i = v.begin(); i != v.end(); i++) cout << *i << ' '; }
我好久以前用 Dev C++ (应该是内置了很古老的 MinGW)写代码的时候,印象中也遇到过这种状况,v.erase(i), i--
这样的操做是有问题的。 erase(i)
会使得 i
及其后面的迭代器失效,从而发生段错误。
但如今 MacOS (clang++ 12), Ubuntu16 (g++ 5.4), Windows (mingw 9.2) 上测试,这段代码都没有问题,而且能输出正确结果。编译选项为:
g++ test.cpp -std=c++11 -O0
实际上也不难理解,由于是连续内存,i
指向的内存位置,在 erase
以后被其余数据覆盖了(这里的行为就跟咱们使用普通数组同样),但该位置仍然在 vector
的有效范围以内。在上述代码中,当 i = v.begin()
时,执行 erase
后,对 i
进行自减操做,这已是一种未定义行为。
我猜应该是 C++11 后(或者是后来的编译器更新),对迭代器失效的这个问题进行了优化。
虽然可以正常运行,但我认为最好仍是严谨一些,更严格地遵循迭代器的使用规则:if (*i==2 || *i==3) i=v.erase(i), i--;
.
如下为各种容器可能会发生迭代器失效的状况:
vector, deque
)
insert(i)
和 erase(i)
会发生数据挪动,使得 i
后的迭代器失效,建议使用 i = erase(i)
获取下一个有效迭代器。vector
自动扩容时,可能会申请一块新的内存并拷贝原数据(也有多是在当前内存的基础上,再扩充一段连续内存),所以全部的迭代器都将失效。list, forward_list
):insert(i)
和 erase(i)
操做不影响其余位置的迭代器,erase(i)
使得迭代器 i
失效,指向数据无效,i = erase(i)
可得到下一个有效迭代器,或者使用 erase(i++)
也可(在进入 erase
操做前已完成自增)。set/map
):与链表型相同。unodered_{set_map}
):与链表型相同。