第九章 摘录

第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时,除了指定元素类型,还要指定容器大小。如:arrayarray的构造函数无法接受大小参数,因为已经在前面定义了。大小是类型的一部分!!

与其他容器不同,一个默认构造的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都不能比较

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> str_stk; //在vector上实现的空栈

stack> str_stk2(svec); //str_stk2是在vector上实现的,初始化时保存svec的拷贝

所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在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允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。

你可能感兴趣的:(第九章 摘录)