9 STL

9.1 双向线性链表容器

9.1.1 基础设施

节点及其指针。

9.1.2 构造、深拷和析构

构造函数初始化空链表,拷贝构造函数和拷贝赋值运算符函数支持深拷贝,析构函数销毁剩余节点。

9.1.3 获取首元素

通过普通容器获取其首元素的左值引用,通过常容器获取其首元素的常左值引用,后者应能够复用前者的实现。

9.1.4 向首部压入

在首节点的前面增加新节点,使新节点成为新的首节点。

9.1.5 从首部弹出

删除首节点,使被删除节点的后节点成为新的首节点。

9.1.6 在链表尾部执行与首部类似的操作

获取尾元素、向尾部压入和从尾部弹出。

9.1.7 删除所有匹配元素

遍历的同时判断是否相等,删除所有满足条件的节点。

9.1.8 清空、判空、大小和输出

9.1.9 第一个测试用例

当用char const*实例化List模板时,其remove函数无法删除与其参数匹配的元素。

9.1.10 针对char const*的特化

将类型相关的操作与类型无关的操作分开。

分别给出equal函数的通用版本和特化版本。

当用char const*实例化List模板时,其remove函数可以删除与其参数匹配的元素。

9.2 迭代器与泛型算法

9.2.1 正向迭代器

9.2.1.1 不用迭代器的遍历

允许用户以类似数组的方式,通过下标访问容器中的元素。

第二个测试用例。

通过下标遍历链表的平均时间复杂度高达O(N2)级。

9.2.1.2 正向迭代器内部类

封装节点的指针,扮演指向容器中元素的指针的角色。

通过任何容器的迭代器访问其中的元素,都与通过指针访问数组中元素无异。允许用户在完全不了解容器内部细节的前提下,以一致且透明的方式,访问其中的元素。

象指针一样支持相等和不等操作符。

象指针一样支持前后自增操作符。

象指针一样支持前后自减操作符。

象指针一样支持解引用和间接成员访问操作符。

在实现了所有这些功能以后,一个迭代器在行为和用法上,便与一个指向容器元素的平凡指针无异。

9.2.1.3 获取起始和终止迭代器

起始正向迭代器指向容器中的首元素,而终止正向迭代器则指向容器中尾元素的下一个位置。

9.2.1.4 基于迭代器的插入

创建新节点,并使迭代器所指向的节点及其前节点成为新建节点的后节点和前节点,返回指向新节点的迭代器。

9.2.1.5 基于迭代器的删除

删除迭代器所指向的节点,令该节点前后节点的后前指针指向其后前节点,返回指向被删除节点之后的迭代器。

9.2.1.6 第三个测试用例

9.2.2 泛型算法

9.2.2.1 线性查找

基于迭代器的线性查找。

第四个测试用例。

9.2.2.2 快速排序

基于迭代器的快速排序,包括通过小于操作符比大小和通过比较器比大小,两个版本。

第五个测试用例。

9.3 向量

9.3.1 基本特性与实例化

9.3.1.1 连续内存与下标访问

9.3.1.2 动态内存分配

9.3.1.3 实例化

9.3.2 迭代器

9.3.2.1 可写迭代器与只读迭代器

9.3.2.2 正向迭代器与反向迭代器

9.3.2.3 顺序迭代器与随机迭代器

9.3.2.4 迭代器的使用

9.3.3 成员函数

9.3.3.1 获取首/尾元素

例如:

9.3.3.2 压入/弹出元素

例如:

9.3.3.3 插入/删除元素

例如:

9.3.3.4 大小和容量

vector类模板提供一系列与大小和容量有关的成员函数:

由图中不难看出,向量的大小和容量具有如下特性:

9.3.4 查找和排序

9.3.4.1 在特定区域内查找

例如:

9.3.4.2 在特定区域内排序

小于比较器可以是形如:

的函数,也可以是实现了小括号操作符的函数对象:

旨在定义容器中元素间的(小于)比较规则,以供排序之需。

一种类型最多只能定义一个小于操作符函数,因此通过小于操作符定义比较规则的灵活性通常较差。例如:

而小于比较器则可以定义多个,可以根据需要选择适用的比较器版本,因此灵活性往往更好。例如:

9.3.5 类类型元素

9.3.5.1 缺省构造

如果一个类类型对象需要被存储在向量中,那么该类至少应支持缺省构造,以确保向量内存的初始化。

9.3.5.2 拷贝构造和拷贝赋值

该类还需要支持完整意义上的拷贝构造和拷贝赋值。

9.3.5.3 小于和等于

该类可能还需要支持“<”和“==”两个关系操作符,用于元素间的小于和等于判断,以支持排序和查找。

作为小于操作符的替代方案,也可以提供一个定义了小于比较规则的小于比较器(函数或函数对象)。

9.4 双端队列与列表

9.4.1 双端队列

9.4.1.1 具有向量的所有功能

双端队列具有向量的所有功能,除了没有capacity和reserve两个成员函数。

9.4.1.2 首尾增删效率一致

在双端队列的头部与在其尾部进行插入(insert)或删除(erase)操作的效率一样高。

9.4.1.3 时空复杂度比向量高

9.4.1.4 可在头部压入和弹出元素

9.4.2 列表

9.4.2.1 链式线性表(链表)

列表是按照链式线性表(链表)的形式进行存储的,元素存放在地址不连续的离散节点中,节点通过前后指针彼此关联,符合数据结构中双向线性链表的结构特征。

9.4.2.2 任意位置增删效率都很高


在列表的任何位置插入或删除元素的效率都很高。离散的链式存储结构决定了在列表中增删元素不需要移动现有节点。所要做的唯一工作仅仅是调整前后节点的指针,以维持链式结构的连续性,而完成这种工作所花费的时间与列表中的元素个数无关,与被增删元素的位置亦无关,即所谓常数时间O(1),这也是选择列表容器的理由之一。

9.4.2.3 禁止随机访问

无法通过下标或迭代器对列表中的元素做随机访问。因为对于列表来说,执行这样的操作至少需要线性时间O(N)。STL的设计哲学是只在容器内部实现具有较好性能表现的操作,而把那些时间或者空间复杂度偏高的工作,留给用户根据实际应用的具体需求量力而为。这总比包揽一切却处处挨骂要好。

STL容器中只有向量和双端队列采用连续内存存放数据元素,只有这两个容器的随机访问(指针计算)可以在常数时间内完成,因此只有这两个容器的迭代器支持随机迭代,同时也只有这两个容器提供下标操作符。其它容器,包括列表,都只能做顺序迭代,而且也不支持下标操作符。

9.4.2.4 删匹配

例如:

9.4.2.5 唯一化

例如:

9.4.2.6 拆分

将参数列表的部分或全部元素剪切到调用列表中。

用两个迭代器表示一个容器中的元素范围,其中的上限迭代器,通常指向该范围中最后一个元素的下一个位置。

列表拆分的时间复杂度为常数级O(1)

9.4.2.7 合并

将有序参数列表中的全部元素,合并到有序的调用列表中,合并后的调用列表依然有序,参数列表为空。

列表合并的过程中并没有排序,因此其时间复杂度仅为线性级O(N)

9.4.2.8 排序

列表有自己的排序函数。

全局函数sort只适用于拥有随机迭代器的容器,列表的迭代器是顺序迭代器,不能使用全局域的sort排序函数。

列表利用自身内存不连续的特点,可以更好的性能实现排序算法函数。

9.5 堆栈、队列和优先队列

9.5.1 堆栈

9.5.1.1 对底层容器的要求

堆栈作为一种适配器容器,可以用任何支持push_back、pop_back、back、size和empty等操作的底层容器进行适配。

9.5.1.2 满足要求即可适配

除了STL的三种线性容器外,程序员自定义的容器,只要提供了正确的接口函数,也可用于适配堆栈。

底层容器
stack
void push_back(value_type const& val)
void pop_back(void)
value_type& back(void)
value_type const& back(void) const
size_type size(void) const
bool empty(void) const
void push(value_type const& val)
void pop(void)
value_type& top(void)
value_type const& top(void) const
size_type size(void) const
bool empty(void) const

适配器模式的本质就是接口翻译,即将一种形式的接口翻译成另一种形式,以下是堆栈容器的简化实现:

9.5.1.3 底层容器的类型

定义堆栈容器时可以指定底层容器的类型。

在C++11以前,两个右尖括号之间至少要留一个空格,否则编译器会把“>>”误解为右移操作符,导致编译错误。

三种线性容器,向量、双端队列和列表中的任何一种都可以作为堆栈的底层容器,其中双端队列是缺省底层容器。

等价于

9.5.2 队列

9.5.2.1 对底层容器的要求

队列作为一种适配器容器,可以用任何支持push_back、pop_front、 back、front、 size和empty等操作的底层容器进行适配。

9.5.2.2 满足要求即可适配

除了STL的三种线性容器外,程序员自定义的容器,只要提供了正确的接口函数,也可用于适配队列。

底层容器
queue
void push_back(value_type const& val)
void pop_front(void)
value_type& back(void)
value_type const& back(void) const
value_type& front(void)
value_type const& front(void) const
size_type size(void) const
bool empty(void) const
void push(value_type const& val)
void pop(void)
value_type& back(void)
value_type const& back(void) const
value_type& front(void)
value_type const& front(void) const
size_type size(void) const
bool empty(void) const

9.5.2.3 底层容器的类型

定义队列容器时可以指定底层容器的类型。

在C++11以前,两个右尖括号之间至少要留一个空格,否则编译器会把“>>”误解为右移操作符,导致编译错误。

三种线性容器中除向量以外的任何一种都可以作为队列的底层容器,其中双端队列是缺省底层容器。

等价于

不能用向量适配队列的原因是,它缺少pop_front接口。

9.5.3 优先队列

9.5.3.1 压入元素大者优先

每当向优先队列中压入一个元素,队列中具有最高优先级的元素会被自动排至队首(类似插入排序)。因此从优先队列中弹出元素,既不是先进先出,也不是后进先出,而是优者先出,即谁的优先级高谁先出队。只有那些优先级相同的元素才遵循和队列一样的先进先出规则。

9.5.3.2 用小于操作符比大小

缺省情况下,优先队列利用元素类型的“<”操作符比较大小,谁大谁的优先级高。

9.5.3.3 用小于比较器比大小

也可以通过小于比较器比较大小,确定优先级高低。

9.5.3.4 底层容器的类型

定义优先队列容器时可以指定底层容器的类型。

在C++11以前,两个右尖括号之间至少要留一个空格,否则编译器会把“>>”误解为右移操作符,导致编译错误。

三种线性容器中除列表以外的任何一种都可以作为优先队列的底层容器,其中双端队列是缺省底层容器。

等价于

不能用列表适配优先队列的原因是,它缺少按优先级调整元素顺序时所需要的随机迭代器。

9.6 映射与多重映射

9.6.1 映射

9.6.1.1 key-value对

映射将现实世界中不同类型事物间的对应关系,抽象为由一系列key-value(键——值)对组成的集合,并提供通过key快速检索与之相对应的value的方法。

value
key
个人信息
考试成绩
个人简历
商品价格
数据库表记录
居民身份证号码
学号
姓名
条形码
主键

9.6.1.2 红黑树

在映射内部,所有的key-value对被组织成以key为基准的红黑树(自平衡有序二叉树)的形式。

映射中的key必须是唯一的,表达一一对应的逻辑关系。

9.6.1.3 根据key找value

基于有序二叉树的结构特征,映射可以在对数时间O(logN)内,根据key找到与之相对应的value。因此映射主要被应用在需要高速检索特性的场合。

具有平衡有序二叉树特征的红黑树,相较于一般有序二叉树的特殊之处在于,其任何节点的左右子树大小之差不会超过1。这样做既有利于保证其搜索性能的均化,又可以有效避免单连枝树向链表退化的风险。

当由于向红黑树中插入或删除节点而导致其平衡性遭到破坏时,必须根据具体情况对树进行一次或多次的单旋或双旋,直至其恢复平衡。因此映射的构建和修改通常较耗时,不适于存储频繁变化的key。

9.6.1.4 以key为索引的下标操作

映射支持以key为索引的下标操作。

返回与参数key相对应的value的引用,若key不存在,则新建一个key-value对,其中的value按缺省方式初始化。

映射的下标运算符既可用于插入又可用于检索。

9.6.1.5 key类型的小于操作符

映射内部为了维护其二叉树结构的有序性,必然需要对key做大小比较。缺省情况下这项工作由key类型的“<”操作符来完成。

注意string类型的“<”操作符是大小写敏感的。

9.6.1.6 key类型的小于比较器

也可以通过针对key类型的小于比较器自定义排序规则。

9.6.1.7 基本单元

映射的基本访问单元和存储单元都是pair,pair只有两个成员变量,first和second,分别表示key和value。

9.6.1.8 插入元素

例如:

9.6.1.9 删除元素

9.6.1.10 查找元素

9.6.2 多重映射

9.6.2.1 允许key重复

允许key重复的映射即为多重映射,表示一对多的逻辑关系。比如下图显示销售员——季度销售额对应关系:

9.6.2.2 不支持下标操作

多重映射无法根据给定的key唯一确定与之对应的value,因此多重映射不支持以key为索引的下标操作。

9.6.2.3 获取匹配下限

例如:

9.6.2.4 获取匹配上限

例如:

9.6.2.5 获取匹配范围

例如:

9.7 集合与多重集合

9.7.1 集合

集合就是没有value的映射,其中的每个元素相当于映射中的key,必须是唯一的。

9.7.2 多重集合

多重集合就是没有value的多重映射,其中的每个元素相当于多重映射中的key,允许重复。

9.8 字符串

C++中的string实际上是basic_string模板用char类型实例化后的类型别名。

需要包含头文件<string>。

9.8.1 实例化和操作符

9.8.1.1 基本用法

9.8.1.2 基本运算

9.8.2 大小和长度

9.8.3 拼接、搜索和子串

9.8.3.1 拼接

9.8.3.2 搜索

9.8.3.3 子串

9.8.4 单字符访问

9.8.5 高级操作

9.8.5.1 查找与替换

9.8.5.2 比较与排序

9.8.5.3 插入与删除

9.8.5.4 交换与复制

9.9 内存分配器

9.10 全局迭代器

9.10.1 I/O流迭代器

9.10.2 插入迭代器

9.11 常用泛型算法

除了查找(find)和排序(sort)算法以外,STL还提供了其它一些算法,用于完成诸如复制、遍历、计数、合并、填充、比较、变换、删除、划分等多种任务

9.11.1 复制算法

9.11.2 遍历算法