第9章 顺序容器
第9章是第3章的扩展。
9.1 顺序容器概述
类型 | |
---|---|
vector | 可变数组大小。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。 |
deque | 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 |
list | 双向链表。只支持双向顺序访问。在list中任何位置进行插入/产出操作速度都很快。 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作都很快 |
array | 固定大小数组。支持快速随机访问。不能添加或者删除元素。 |
string | 与vector相似的容器,但专门用于保存字符。随机访问块。在尾部插入/删除速度快。 |
forward_list和array是新c++标准增加的类型。
list和forward_list这两个容器与vector、deque和array相比,额外内存开销很大。
forward_list
其设计目标是达到与最好的手写的单向链表相当的性能。所以它没有size操作,因为保存或计算大小就会比手写链表多出额外的开销。
array
与内置数组相比,array更安全、更易用。新标准库的容器比旧版本的快得多。
现代C++程序应该使用标准库容器而不是更原始的数据结构,如内置数组。
确定使用哪种顺序容器
从P293可以看到,vector是最推荐的。array好像根本没提到。。。
当程序既需要随机访问元素,又需要在容器中间位置插入元素,那使用哪种就取决于在list或forward_list中访问元素与vector或deque中插入/删除元素的相对性能。一般来说,应用中占主导地位的操作决定了容器类型的操作。
9.2 容器库概览
某些操作是容器共有的,某些则只适用于一小部分容器。
容器都定义在一个同名的头文件中。
容器均定义为模板类,对大多数容器(并非所有),需要提供额外信息来生成。
容器操作
类型别名 | |
---|---|
iterator | 此容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
difference_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型;与value_type&含义相同 |
const_reference | 元素的const左值类型(即,const value_type) |
构造函数 | |
C c; | 默认构造函数,构造空容器(array见P301) |
C c1(c2); | 构造c2的拷贝c1 |
C c(b, e); | 构造c,将迭代器b和e指定的范围内的元素拷贝到c(array不支持) |
C c{a, b, c…}; | 列表初始化c |
赋值与swap | |
c1 = c2 | 将c1中的元素替换为c2中元素 |
c1 = {a, b, c...} | 将c1中的元素替换为列表中元素 |
a.swap(b) | 交换a和b元素 |
swap(a, b) | 同上 |
大小 | |
c.size() | c中元素数目(不支持forward_list) |
c.max_size() | c中可保存的最大元素数目 |
c.empty() | c是否为空 |
添加/删除元素(不适用于array) | 注:在不同容器中,这些操作的接口都不同 |
c.insert(args) | 将args中的元素拷贝进c |
c.emplace(inits) | 使用inits构造c中的一个元素 |
c.erase(args) | 删除args指定的元素 |
c.clear() | 删除c中的所有元素,返回void |
关系运算符 | |
==, != | 所有容器都支持 |
<, <=, >, >= | 关系运算符(无序关联容器不支持) |
获取迭代器 | |
c.begin(), c.end() | 返回指向c的首元素和尾元素之后位置的迭代器 |
c.cbegin(), c.cend() | 返回const_iterator |
反向容器的额外成员 | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin(), c.rend() | 返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin(), c.crend() | 返回const_reverse_iterator |
9.2.1 迭代器
迭代器支持的操作在P96。算术运算在P99,这些运算只能应用于string、vector、deque和array的迭代器,即list什么的它们的迭代器不能用+,-,>,<之类的算术运算符。
迭代器范围
迭代器范围由一对迭代器表示。end用来指向尾后元素,[begin,end)被称为左闭合区间。
对于迭代器范围的要求:
- 指向同一容器中的元素,或是容器最后一个元素之后的位置
- end不在begin之前
9.2.2 容器类型成员
size_type、difference_type、iterator、const_iterator、value_type、reference、const_reference。最后三个类型介绍在后面的章节,它们在泛型编程中非常有用。
9.2.3 begin和end成员
begin和end有多个版本:带r的版本返回反向迭代器,以c开头的版本返回const迭代器。
不以c开头的都是被重载过的,即实际上含有两个名为begin的成员。当一个非常量对象调用这些变量时,返回的是iterator;只有在对一个const对象调用时,才会返回const_iterator。
9.2.4 容器定义和初始化
将一个容器初始化为另一个容器的拷贝
有两种方法:
- 直接拷贝整个容器
- 拷贝由一个迭代器对指定的元素范围
当为 直接拷贝整个容器 时,容器类型及元素类型都必须相同。
当为拷贝由一个迭代器对指定的元素范围时,就不要求容器类型相同了,只要其元素类型能够成功转换即可。如:
vector articles = {"a","an","the"};
vector words(articles); //错误
forward_list words(articles.begin(), articles.end()) //正确:可将const char*元素转为string
列表初始化
与顺序容器大小相关的构造函数
接受一个容器大小和一个(可选的)元素初始值,这个可选的前提是这个元素类型有默认构造函数,否则必须制定一个显式的元素初始值。
标准库array具有固定大小
定义一个array时,除了指定元素类型,还要指定容器大小。如:array
。array的构造函数无法接受大小参数,因为已经在前面定义了。大小是类型的一部分!!
与其他容器不同,一个默认构造的array是非空的:包含了与其大小一样多的元素,且这些元素都被默认初始化(与内置数组一样)。
array可以进行拷贝或对象赋值操作,而内置数组不行。
9.2.5 赋值和swap
容器赋值运算 | |
---|---|
c1=c2 | 将c1中元素替换为c2中元素的拷贝。c1和c2必须具有相同的类型 |
c={a,b,c...} | 将c1中元素替换为初始化列表中元素的拷贝(array不适用)?(实际上程序能运行) |
swap(c1,c2) | 交换c1和c2中的元素。c1和c2必须具有相同的类型。 |
c1.swap(c2) | swap通常比从c2向c1拷贝元素快得多 |
seq.assign(b,e) | 将seq中元素替换为迭代器b和e所表示的范围中的元素 |
seq.assign(il) | 将seq中元素替换为初始化列表il中的元素 |
seq.assign(n,t) | 将seq中元素替换为n个值为t的元素 |
assign不适用于关联容器和array。
使用assign(顺序容器)
赋值运算符要求左右两边的运算对象具有相同的类型。而assign允许从一个不同但相容的类型赋值,如将vector中一段char*值赋予一个list中的string。
使用swap
除array外,交换两个容器内容的操作保证会很快--元素本身并未交换,swap只是交换了两个容器的内部数据结构。除array外,swap不对任何元素进行拷贝、删除或者插入操作,因此可保证在常数时间内完成。
元素不会被移动,意味着指向容器的迭代器、指针和引用在swap之后都不会失效,仍指向swap操作之前所指向的那些元素。与其他容器不同,对一个string调用swap会导致它们失效。如假定iter在swap之前指向svec1[3]的string,swap之后它指向svec2[3]的元素。
统一使用非成员版本的swap是一个好习惯。
9.26 容器大小操作
forward_list支持max_size和empty,但不支持size。
9.27 关系运算符
每个容器类型都支持相等运算符;除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。
关系运算符左右的对象必须满足容器类型相同、元素类型相同的条件。vector
vector v1 = {1,3,5,7,9,12};
vector v2 = {1,3,9};
vector v3 = {1,3,5,7};
v1 < v2 //true; v1和v2在元素[2]处不同:v1[2]小于等于v2[2]
v1 < v3 //false; v3元素数目更少
容器的关系运算符使用元素的关系运算符完成比较
9.3 顺序容器操作
顺序容器和关联容器的不同之处在于两者组织元素的方式。
容器元素是拷贝,而不是对象本身,与提供值的对象之间没有任何关联。
9.3.1 向顺序容器添加元素
使用push_back
除array和forward_list都支持push_back。
使用push_front
list、forward_list和deque容器支持。插入元素到头部。
在容器中的特定位置添加元素
push_back
插入范围内元素
若传递给insert一对迭代器,它们不能指向添加元素的目标容器。
使用insert的返回值
在新标准下,接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器。若范围为空,不插入任何元素,insert操作会将第一个参数返回。
list lst;
auto iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter, word); //等价于调用push_front
使用emplace操作
emplace、emplace_front、emplace_back分别对应insert、push_front和push_back。
但emplace都是构造而不是拷贝元素。emplace成员会使用传递的参数在容器管理的内存空间中直接构造元素。若参数为空,则调用元素的默认构造函数。
如:
//c为容器,保存Sales_data
//使用三个参数的Sales_data构造函数
c.emplace_back("978-0590353403", 25, 15.99)
//错误:没有接受三个参数的push_back版本
c.push_back("978-0590353403", 25, 15.99)
//正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sales_data("978-0590353403", 25, 15.99))
如果要插入的对象还未定义,emplace的好处就体现出来了,第三种方法要创建临时变量显然没那么高效。
传递给emplace函数的参数必须和元素类型的构造函数相匹配。
练习9.21 — 为何vector支持通过insert不断在首位置插入元素,却不支持push_front?
Milo Yip:因为 vector 的设计是为了 O(1) push_back(),对它来说 push_front() 的性能等同于 O(n) 的 insert(),所以就不提供 push_front() 避免误用。 类似 vector 但能 O(1) push_front() 的是 deque。
陈硕:这正是 STL 接口(concept)的优点,也是泛型编程优于面向对象的方面:明确的时间复杂度,不提供看似高效实则低效的操作,以免误用(例如 list 就没有 operator[])。
反观某些以 OO 风格实现的数据结构库,例如 Java collections,哪怕是 LinkedList 也要实现 List 接口的 get 方法,是线性时间。如果你面向接口编程,对传入的 List 调用 get ,而用户传给你一个链表,那就呵呵了,有可能 O(N) 算法变成 O(N^2)。全世界不知道有多少 CPU cycles 浪费在了 LinkedList.get() 中。究其原因,OO interface 只规定了功能,没有规定性能(复杂度),因此,OO 不适合描述数据结构(ADT)。
最后,为了防止这种错误,究竟是不应该面向接口编程,还是 LinkedList 不应该实现 List 接口,又或者 List 接口不应该包含 get() 呢?
Java LinkedList 的缺点不止这一点,谁用谁知道。
9.3.2 访问元素
使用front和back分别返回首元素和尾元素的引用。如果使用迭代器获取尾元素的值,就得*(c.end()--)
,而用back就很方便。
在调用front和back之前要确保c非空,否则操作的行为将是未定义的。
还有c[n]和c.at(n)都为返回下标为n的元素的引用。
9.3.3 删除元素
9.3.4 特殊的forward_list操作
9.3.5 改变容器大小
9.3.6 容器操作可能使迭代器失效
9.4 vector对象是如何增长的
为支持快速随机访问,vector将元素连续存储。当向vector添加元素时,如果没有空间容纳新元素,容器就必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中再添加新元素,释放旧存储空间。
这个过程是缓慢的,为了保证效率,当超出容器空间时,vector和string的实现通常会分配比新的空间需求大的内存空间。(我的电脑编译显示是之前容量的一倍,这个新空间大小取决于不同的编译器)。
resize成员函数只改变容器中元素的数目,而不是容器的容量。
容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
shrink_to_fit()是可以用来要求容器将超出当前大小的多余内存退回给系统,调用它只是一个请求,标准库并不保证退还内存。
reserve(n) :分配至少能容纳n个元素的内存空间。
9.5 额外的string操作
9.5.1 构造string的其他方法
string s(cp,n) | s是cp指向的数组中前n个字符的拷贝。数组至少包含n个字符 |
---|---|
string s(s2,pos2) | s为s2从下标pos2开始的字符的拷贝。若pos2>s2.size()则构造函数的行为未定义,会抛出out_of_range异常(P173)。 |
string s(s2,pos2,len2) | len2可以很大,但不管它值为多少,构造函数至多拷贝s2.size()-pos2个字符 |
substr操作
返回一个string,它是原始string的一部分或全部的拷贝。
s.substr(pos, n)
,n为长度。和上面的构造string的方法道理是相同的。当pos>s.size()时,抛出异常。
9.5.2 改变string的其他方法
insert和eraser除了接受迭代器的版本,string还提供了接受下标的版本。
s.insert(s.size(), 5, '!'); //在s末尾插入5个感叹号
s.erase(s.size()-5 , 5); //从s删除最后5个字符
append和replace函数
这是string类定义的两个额外的成员函数。
s.append(args) //将args追加到s。返回一个指向s的引用
s.replace(range,args) //删除s中范围range内的字符,替换为args指定的字符。返回一个指向s的引用
。
s.assign(args) //将s中的字符替换为args指定的字符。返回一个指向s的引用
s.erase(pos,len) //删除从位置pos开始的len个字符。若len省略,则删除从pos至末尾的所有字符。
s.insert(pos,args) //在pos之前插入args指定的字符。
9.5.3 string搜索操作
s.find(args) //查找s中args第一次出现的位置
s.rfind(args) //查找s中args最后一次出现的位置
s.find_first_of(args) //在s中查找args中任何一个字符第一次出现的位置
s.find_first_not_of(args) //在s中查找args中任何一个字符最后一次出现的位置
s.find_last_of(args) //在s中查找第一个不在args中的字符
s.find_last_nof_of(args) //在s中查找最后一个不在args中的字符
args必须为以下形式:
c,pos 从s中位置pos开始查找字符c。pos默认为0
s2,pos 从s中位置pos开始查找字符串s2。pos默认为0
cp,pos 从s中位置pos开始查找指针cp指向的以空字符串结尾的C风格字符串。pos默认为0
cp,pos,n 从s中位置pos开始查找指针cp指向的数组的前n个字符。pos和n无默认值
pos可以用于指定在哪里开始搜索
逆向搜索
提供从右向左的搜索。就是rfind,find_last_of及find_last_not_of函数。
9.5.4 compare函数
与C标准库的strcmp函数很相似。类似strcmp,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、正数或负数。
9.5.5 数值转换
to_string(val) | val可以是任何算术类型。 |
---|---|
stoi(s,p,b) | b表示转换所用的基数,默认为10. |
stol(s,p,b) | p是size_t指针,用来保存s中第一个非数值字符的下标,默认为0。 |
stoll(s,p,b) | |
stoul(s,p,b) | |
stoull(s,p,b) | |
stof(s,p) | |
stod(s,p) | |
stold(s,p) |
9.6 容器适配器
标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。
容器、迭代器和函数都有适配器。
本质上,一个适配器是一种机制,能使某种事物的行为看起来像另一个事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
定义一个适配器
每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。
stack
stack
所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在array之上。类似的,我们也不能用forward_list来构造适配器,所有适配器都要求容器具有添加和删除及访问尾元素的能力。
举例:
stack只要求push_back、pop_back、back操作,所以它支持除array和forward_list之外的任何容器类型来转换。
queue要求back、push_back、front、push_front,因此它可以构造在list或deque之上,但不能基于vector。
priority_queue除了front、pop_back、push_back操作之外还要求随机访问能力,因此可以构造于vector或deque之上,但不能基于list。
栈适配器 - 默认基于deque实现,也可以在list或vector之上实现。
s.pop() //删除栈顶元素
s.push(item) //创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者
s.emplace(args) //由args构造
s.top() //返回栈顶元素,但不将元素弹出栈
队列适配器
queue采用先进先出(FIFO)的存储和访问策略。
queue默认基于deque实现,priority_queue默认基于vector实现。
q.pop() //返回queue的首元素或priortiy_queue的最高优先级的元素,但不删除此元素。
q.front() //返回首元素或尾元素,但不删除此元素。
q.back() //只适用于queue
q.top() //只适用于priority_queue,返回最高优先级元素,但不删除。
q.push(item)
q.emplace(args)
priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。