STL (Standard Template Library) 标准模板库是 C++ 的内置库,提供了一系列常用的数据结构与算法,而且在使用的时候无需手动管理内存。这篇文章会先介绍 STL 的各个部件与设计思路,然后总结各种容器的基本性质和底层实现。
STL 简介
STL 有六大组件:容器,算法,迭代器,仿函数,配接器,配置器。STL 的核心是泛型容器和算法,可以方便地储存和操作各种类型。STL 组件耦合度低,复用性高。
- 容器 (Container) : 常用的数据结构,如 vector, list, deque, set, map 等,用来存放数据。
- 算法 (Algorithm) : 常用的算法,如 sort, find, remove 等,支持各种容器类型。
- 迭代器 (Iterator) : 是一种“泛型指针”,可以在各种类型上做类指针的操作。
- 仿函数 (Functor) : 统一的可调用对象,可以传入泛型算法中作为策略使用。
- 配接器 (Adapter) : 用来修饰容器,使容器表现出另一种行为,例如 queue, stack, 底层实际是 deque。
- 配置器 (Allocator) : 负责空间配置与管理,自动管理容器所使用的空间。
STL 内存管理
STL 对象的内存管理由 std::allocator 负责,会自动进行容器内对象构造前的内存申请与析构后的内存释放。STL 内存管理的思路如下:
- 考虑了多线程状态
- 内存不足时自动向 system heap 要空间
- 考虑了内存碎片问题
其中,内存碎片是通过双层级配置器来解决的。如果申请的内存大于 128 字节,那么通过第一层配置器直接向系统申请。如果小于 128 字节,则交给第二级配置器。第二级配置器管理了一个 free-list,通过这个 free-list 管理了一个小区块内存池。
STL容器基本性质表
顺序容器及适配器
容器 | 性质 | 访问 | 查找 | 插入 | 删除 | 其他 |
---|---|---|---|---|---|---|
vector | 可变数组 | 随机访问 | O(1) | O(n) | O(n) | 尾部插入删除O(1) |
string | 字符可变数组 | 随机访问 | O(1) | O(n) | O(n) | 尾部插入删除O(1) |
priority_queue | 堆 | O(logn) | O(logn) | O(nlogn) | O(nlogn) | 排序队列 |
list | 双向链表 | 双向顺序访问 | O(n) | O(n) | O(n) | 插入/删除比较灵活 |
forward_list | 单向链表 | 单向顺序访问 | O(n) | O(n) | O(n) | 插入/删除灵活 |
deque | 双端队列 | 随机访问 | O(1) | O(n) | O(n) | 头尾插入删除O(1) |
queue | 单端队列 | 随机访问 | O(1) | O(n) | O(n) | 尾部插入删除O(1) |
stack | 栈 | 随机访问 | O(1) | O(n) | O(n) | |
array | 固定大小数组 | 随机访问 | O(1) | - | - | 不能插入/删除 |
关联容器
容器 | 性质 | 访问 | 查找 | 插入 | 删除 | 其他 |
---|---|---|---|---|---|---|
map | 有序字典 | O(logn) | O(logn) | O(logn) | O(logn) | |
set | 有序集合 | O(logn) | O(logn) | O(logn) | O(logn) | |
multimap | 有序字典 | O(logn) | O(logn) | O(logn) | O(logn) | 可重复 |
multiset | 有序集合 | O(logn) | O(logn) | O(logn) | O(logn) | 可重复 |
unordered_map | 无序字典 | O(1) | O(1) | O(1) | O(1) | |
unordered_set | 无序集合 | O(1) | O(1) | O(1) | O(1) | |
unordered_multimap | 无序字典 | O(1) | O(1) | O(1) | O(1) | 可重复 |
unordered_multiset | 无序集合 | O(1) | O(1) | O(1) | O(1) | 可重复 |
STL容器介绍
连续内存顺序容器
vector
vector是可变大小的数组,允许插入和删除,是在 C++ 里经常用来代替 C 数组的一种容器,支持快速随机访问。
vector 自行维护了一段内存,当插入时内存不够时,会重新申请一段更大的内存,并把目前的数据移动到新内存里,再进行插入。不同编译器的扩容倍数不一样,有 1.5 倍或 2 倍的,1.5倍造成的内存碎片更少。
string
string 的底层就是 vector<char>
,但 string 本身还提供了许多字符串操作函数,比如append
, substr
, replace
, find
, compare
, to_string
, stoi
等。
priority_queue
priority_queue 是优先队列,放入其中的元素会自动排序,一般都是做最大堆/最小堆使用的,STL 提供了 push
, pop
, top
三种基本操作。
priority_queue 是一种适配器,是对 vector 容器的再封装。priority_queue 以 vector 为底层容器,使用堆 heap 作为处理规则。
半连续内存顺序容器
deque
deque 是双端队列,可以从首尾快速增删,也支持用下标随机访问元素,并不是完全封装的队列,提供了很大的方便。
deque 的底层是一小块连续空间,该空间每个元素都是指向另一段连续内存的指针,相当于 deque 管理的是一段内存映射表。所以,访问 deque 的元素需要经过两次查找,访问速度要比vector慢。
stack & queue
stack 是栈,queue 是单端队列。
stack 和 queue 都是适配器,它们的底层一般是 deque。不用 vector 作为底层的原因可能是因为扩容比较耗时。
非连续内存顺序容器
list & forward_list
list 是双向链表,forward_list 是单向链表,两者的底层也是链表,可以快速增删。
list 的插入,删除,迭代器都和连续内存容器不一样,lst.before_begin()
, lst.insert_after()
, lst.erase_after()
。另外,<algorithm>
的泛型算法对 list 不适用。list 有专用版本,例如:lst.merge
, lst.sort
, lst.remove
, lst.reverse
,lst.unique
, lst.splice
等。
有序关联容器
set
set 是集合,有序排列,默认从小到大,不允许重复的关键字。不允许用下标访问,只能用迭代器直接访问元素。
multiset 是允许重复关键字的 set。STL 还为 set 提供了一些集合运算的函数,如交集set_intersection
、并集set_union
、差集set_difference
等。
map
map 是字典,存储的为 {key, value}
对,可以通过关键字来查找值。map 可以通过下标运算符来建立元素,例如 m[3]++;
,若之前不存在 key 为 3 的元素,则这条语句会直接创建 key 为 3 的元素,再自增其 value。
有序关联容器都具有equal_range
, lower_bound
, upper_bound
, merge
, count
等自带算法。
红黑树
有序关联容器的内部结构都是红黑树 (Red Black Tree, RB-tree)。
红黑树是一种二叉查找树,也是一种平衡二叉树。红黑是可以用 O(logn) 的时间复杂度进行查找,插入,删除操作。红黑树是统计性能较高的平衡二叉树,被广泛应用于存储有序数据的场景。
红黑树的平衡并不严格,其只要求树的最长路径不大于两倍最短路径的长度,但这也比 BST 好很多,可以保证 O(logn) 的复杂度,不会像 BST 可能退化到 O(n)。
红黑树的不平衡在 3 次旋转之内就可以回归平衡,这让它的统计性能比 AVL 树更高。
红黑树与哈希表对比
- 红黑树有序,hash 表无序
- hash 表查找速度比红黑树快,复杂度为是 O(1) ,但 hash 表会在 hash 函数上消耗时间,数据量小时,hash 表不一定比红黑树快。而当 hash 表的数据量极大时,可能会因为 hash 函数的质量导致插入/查找元素的时间低于红黑树。
- hash 表可能会消耗多余的内存,而红黑树不会。
- 如果数据是静态的,可以使用红黑树,如果数据是动态的,红黑树具有更好的统计性能。
无序关联容器
无序关联容器有四种,unordered_set,unordered_map,unordered_multiset,unordered_multimap。
无序容器的底层是哈希表,STL 的哈希表是由 vector 和 list 组成的。当空间不够的时候会倍增,表中所有数据在倍增后会重新 hash。STL 也允许自行传入 hash 函数和重组存储。
应用场合
- 不增删,静态存储,C 数组或者 array(STL 的静态数组)
- 需要动态增长,vector
- 增删大部分在头尾,deque
- 有大量增删,list
- 其他的容器比较有特点,按需使用
迭代器失效
- vector, string
- 插入时,如果存储空间重新分配,可能都失效,若没有重新分配,则插入之后迭代器,指针,引用可能失效。
- 删除时,删除位置之后的迭代器,指针,引用都失效。
- deque
- 在首尾插入/删除,迭代器失效,但引用,指针不失效。
- 在其他位置插入,迭代器,指针,引用都可能失效
- 在其他位置删除,其他迭代器,引用,指针代都失效
- list, forward_list
- 插入删除后迭代器,指针,引用仍然有效 (删除的那一个失效)
使用迭代器的策略:
- 最好每次增删或容量调整后重新定位现有迭代器
- 不要保存 end() 返回的迭代器,若元素有增删,每次都要调用 end() 进行判断
- 循环内使用 insert,erase 时要算清楚循环迭代器的位置,以及不同情况下是否该自增/自减