顺序容器
所有容器类都共享公共的接口,不同容器按不同的方式进行扩展,这个公共接口使得学习容器更加容器。我们基于这种容器所学习的内容也都适用于其他容器。每种容器都提供了不同的性能和功能权衡
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器的位置相对应。
顺序容器概述
所谓的顺序容器是指,在内存中数据存储有一定顺序。数据结构中的顺序容器有:可变数组、队列、数组、链表、栈。
c++ 标准库中的顺序容器提供了快速顺序访问元素的能力。但是这些容器在一下方面都有不同的性能折中
- 向容器中添加或者删除元素的代价
- 非顺序访问容器中元素的代价
标准库中顺序容器主要有:
- vector:可变大小的数组。支持快速随机访问,在尾部之外插入或者删除元素可能会很慢
- dque:双端队列,支持快速随机访问,在头尾位置插入/删除元素速度很快
- list:双向连标,只支持双向顺序访问,在list中任何位置进行插入删除操作速度都很快
- forward_list: 单向链表,只支持单向顺序访问,在链表任何位置插入删除元素速度很快
- array: 固定大小的数组,支持快速随机访问,不能添加或者删除元素
- string: 与vector容器类似,但专门用于保存字符,随机访问快。在尾部插入与删除速度快
c++ 标准库中的容器是经过精心优化设计过的。性能通常会是同类数据结构中最好的。现代c++ 程序应该使用标准库容器,而不是原始的数据结构(如内置数组)
通常使用vector 是最好的选择,除非你有很好的理由选择其他容器
一下是一些选择容器的基本原则:
- 除非你有很好的理由选择其他容器,否则使用vector
- 如果你的程序有很多小的元素,且额外开销很严重,否则不要使用list或者forward_list
- 如果程序要求随机访问元素,应该使用vector 或者deque
- 如果程序要求在容器中间插入或者删除元素,应该使用list或者forward_list
- 如果程序要在头尾位置插入或者删除元素,但是不会在中间位置插入删除元素,则应该使用deque
- 如果程序只有在读取输入时才需要在容器中间插入元素,随后需要随机访问元素,则:
6.1 首先确定是否真的需要在容器中间位置添加元素。当处理输入数据时通常很容易向vector中添加数据,然后再调用标准库的sort函数,来重排元素,避免在中间位置添加元素
6.2 如果必须在中间位置插入元素考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中
如果你不确定该使用哪种容器,可以在程序中只使用vector 和list公共的操作,不使用下标操作,使用迭代器,避免随机访问
容器库概述
迭代器
迭代器是访问容器中元素的公共接口
所有迭代器都通过解引用运算符来实现这个操作。标准库中的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。部分容器的迭代器也定义了递减运算符,用于从一个元素移动到上一个元素
一个迭代器范围是由一对迭代器来表示的。两个迭代器分别指向同一个容器的元素或者尾元素之后的位置。
如果两个迭代器满足以下两个条件,则这两个迭代器构成一个迭代器范围:
- 它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置
- 我们可以通过反复递增begin 来到达end位置,换句话说,end不在begin之前
如果两个迭代器构成一个迭代器范围,则:
- 如果begin和end相等,则范围为空
- 如果begin和end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
- 我们可以对begin递增若干次,使得begin== end
容器类型成员
每个容器都定义了多个类型,我们已经使用了其中的3种:size_type、iterator、const_iterator
除了正向的迭代器,容器库还提供了反向遍历容器的迭代器,反向迭代器就是一种反向遍历容器的迭代器。与正向迭代器相比各种操作的含义也都发生了颠倒。例如对一个反向迭代器执行++操作,会得到上一个元素。
剩下的就是类型别名了。通过类型别名,我们可以在不了解容器中元素类型的情况下使用它,如果需要元素类型可以使用容器的value_type ,如果需要元素类型的一个引用可以使用reference或者const_reference。
begin 和 end 成员
begin
和 end
操作生成一个指向容器中第一个元素和尾元素之后位置的迭代器范围。begin
和end
有多个版本。带r的版本返回反向迭代器,带c的版本返回const型迭代器
容器定义和初始化
可以将一个容器初始化为另一个容器的拷贝
将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者拷贝由一个迭代器对指定的元素范围
为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且新容器和原容器中的元素类型也可以不同,只要能将拷贝的元素转化为要初始化的容器的元素类型即可
在新标准中我们对一个容器进行列表初始化
标准库array在定义之处就应该给出具体的大小,而且后续不允许修改它的大小
我们不能直接对内置数组执行拷贝或者对象赋值操作,但是array对象允许
赋值和swap
我们直接使用 = 运算符来将一个容器赋值为另一个容器的拷贝,但是要求容器类型完全相同,array也支持这种操作
顺序容器(array除外)还定义了一个assign的成员,assign操作用参数所指定的元素替换左边容器中的所有元素
list<string> name;
vector<const char*> oldstyle;
name = oldstyle; //错误 = 要求两侧容器类型完全相同
name.assign(oldstyle);
可以使用swap交换两个容器中的内容,要求两个容器类型完全相同。除了array外,swap不对任何元素进行拷贝、删除或者插入操作,可以保证在常量时间内完成
容器大小操作
每个容器都有三大与大小相关的操作。
- size: 返回容器中元素数目
- empty: 当容器中元素数量为0时,返回
true
,否则返回false
- max_size: 返回一个大于或者等于该类型容器所能容纳的最大元素数的值
关系运算符
除了无序容器外的所有容器都支持关系运算符
关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素
比较的过程与比较string大小的过程类似
- 如果两个容器具有相同大小且所有元素都两辆对应相等,则两个容器相等。否则两个容器不等
- 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中对应元素。则较小容器大于较大容器
- 如果两个容器都不是另一个容器的前缀自序列,则它们的比较结果取决于第一个不相等的元素的比较结果
容器的相等运算符实际上是使用元素的==
运算符实现比较的。而其他关系是使用元素的<
运算符
顺序容器的操作
向顺序容器中添加元素
- push_back:将内容追加到容器尾部
- push_front: 将内容添加到容器的首部
- insert: 在容器的特定位置插入0个或者多个元素,返回插入元素位置的迭代器
- emplace_back、emplace_front、emplace: 这些函数是直接在容器内部进行元素构造,而上述函数是将内容进行拷贝。从效率上讲emplace 函数会高一些
使用这些操作时必须记得不同类型的容器使用不同的元素分配策略,而这些策略直接影响性能。
访问顺序容器
每个顺序容器中都有一个front 函数,返回容器内第一个元素的引用。而除了forward_list 之外的所有顺序容器都有一个back成员函数。
另外可以使用at来访问随机位置的元素
记住,这些访问函数返回的都是引用
删除元素
- pop_front: 删除首元素
- pop_back: 删除尾元素
- erase: 可以从容器中删除指定位置的元素,可以传入一个范围,删除指定范围内的元素
特殊的 forward_list 操作
在对forward_list 进行增删操作的时候,需要找到对应位置的前驱节点,而单向链表无法很容器的找到一个节点的前驱节点。因此对单向链表,提供了类似 insert_after、emplace_after、erase_after等操作
改变容器大小
可以使用resize 来增大或者缩小容器大小,如果是缩小容器大小,则指向被删除元素的迭代器、引用、指针都会失效
容器操作可能使迭代器失效
在向容器中添加元素后:
- 如果容器是vector或者string,且存储空间被重新分配,则指向容器的迭代器、指针都会失效。如果存储空间未重新分配,指向插入位置之前的迭代器、指针、引用仍然有效,但是指向插入位置之后元素的迭代器、指针和引用将会失效
- 对于deque,插入到首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效
- 对于list和forward_list,指向容器的迭代器、指针和引用仍然有效
删除一个元素后,指向原来被删除元素的迭代器、指针和引用都会失效。
- 对于forward_list 和list来说,指向容器其他位置的迭代器、引用和指针仍然有效
- 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素以外的其他元素的迭代器、引用和指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效。但是其他迭代器、引用和指针不受影响,如果删除首元素,这些也不会受到影响
- 对于vector和string,指向被删除元素之前元素的迭代器、引用和指针仍然有效
删除元素时尾后迭代器总是会失效
使用insert插入元素后可以保存返回的迭代器,然后用该迭代器进行迭代可以保证迭代器有效
不要保存end返回的迭代器
vector 容器是如何增长的
为了支持快速随机访问,vector 将元素连续存储。如果往容器中添加一个新元素时,发现容器空间已经不够了,就需要重新分配空间。并将已有元素逐一拷贝到新的内存空间中,然后添加新元素。
为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间
vector和string也提供了一些成员函数,允许我们与它的实现中内存分配部分互动。
- capacity: 告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素
- reserve: 允许我们同志容器它应该准备保存多少个元素
一般来讲,vector
的实现采用的策略似乎是在每次需要分配新内容空间时将当前容量翻倍
额外的string操作
除了顺序容器共同的操作之外,string类还提供了一些额外的操作。
这些操作中的大部分要么是提供string类和C风格字符串之间的互相转换,要么是增加了允许我们用下标代替迭代器的版本。
- substr: 返回一个string,它是原始string的一部分或者全部的拷贝
- 可以使用insert、erase、assign 来改变字符串的内容
- append可以在字符串尾部添加一个新字符串; replace 进行查找替换
搜索操作
标准库中提供了6个不同的搜索函数,每个函数有4中不同形式的重载版本。每个搜索操作都返回string::size_type 值,表示匹配发生位置的下标。如果搜索失败返回一个名为string::npos 的static成员
- s.find(arg): 查找字符串中第一次出现某个字符串的位置
- s.rfind(arg): 查找字符串中最后一次出现某个字符串的位置
- s.find_first_of(arg): 在s中查找arg中任意一个字符第一次出现的位置
- s.find_last_of(arg): 在s中查找arg中任意一个字符最后一次出现的位置
- s.find_first_not_of(arg): 在s中查找第一个不在arg 中的字符
- s.find_last_not_of(arg): 在s中查找最后一个不在arg 中的字符
compare 函数
compare函数用于比较两个大小字符串,与C标准库中的strcmp类似
数值转化
- to_string: 将数值数据转化为字符串
- stod: 将字符串转化为double
- stof: 将字符串转化为float
- stoi: 将字符串转化为int
- stol: 将字符串转化为long
- stoul: 将字符串转化为 unsigned long
- stoll: 将字符串转化为 long long
- stoull: 将字符串转化为 unsigned long long
- stold: 将字符串转化为 long double
容器适配器
适配器是标准库提供的一组概念,能使某种事物的行为看起来像另一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像另一种事物一样
标准库提供了三种适配器: stack、queue、priority_queue(优先级队列)
所有的适配器都要求容器具有添加和删除元素的能力。
stack 只要求类型容器具有 push_back、pop_back 操作因此可以使用除了array 和 forward_list 之外的任何容器类型来进行构造
queue 要求容器类型具有 back、push_back、front、pop_back 因此它可以构造在list 或者deque之上但不能基于vector 构造;
priority_queue 要求容器类型具有push_back、front、pop_back之外还要求容器具有随机访问的能力,所以它必须构造在vector之上