Skip to content

Latest commit

 

History

History
435 lines (301 loc) · 21.4 KB

17-顺序容器.md

File metadata and controls

435 lines (301 loc) · 21.4 KB

顺序容器


1 顺序容器概述

C++提供了以下容器:

  • vector:可变大小的数组,支持随机访问,尾部之外的位置插入元素速度可能很慢
  • deque:双端队列,支持随机访问,头尾位置插入元素速度快
  • list:双向链表
  • forward_list:单向链表,forward_list设计目的是达到与手写单向链表数据结构相当的性能
  • array:固定大小的数组,不同添加和删除元素,相比数组,array是一种更加安全更易使用的数组类型
  • string:与vector类似的数组,专门用于存储char

通常情况下应该使用vector,除非你有更好的选择


2 容器库概述

容器上的操作可以整理为:

  • 所有容器都支持的操作
  • 针对特定容器支持的操作
  • 大多数元素都可以保存在容器中,但某些容器的操作对容器元素的类型有特殊要求

类类型没有默认构造函数时,存储该类型容器的初始化:

//假设noDefault没有默认的构造函数
vector<noDefault> v1(10, init);//正确,提供了初始化器
vector<noDefault> v2(10);//错误,必须提供初始化器

C++ 语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足以下两个约束:

  • 元素类型必须支持赋值运算
  • 元素类型的对象必须可以复制

引用类型、输入输出(IO)标准库类型、auto_ptr类型,这些类型不能作为容器元素。

2.1 迭代器

  • 构成范围的begin和end迭代器
  • forward-list迭代器不支持--操作
  • 迭代器是左闭右开的区间
  • last不指向任何元素,而是一个尾部标识
c.begin()    返回一个迭代器,它指向容器 c 的第一个元素
c.end()    返回一个迭代器,它指向容器 c 的最后一个元素的下一位置
c.rbegin()    返回一个逆序迭代器,它指向容器 c 的最后一个元素
c.rend()    返回一个逆序迭代器,它指向容器 c 的第一个元素前面的位置
c.cbegin()、c.cend()

由 end 操作返回的迭代器指向 vector 的末端元素的下一个 。“超出末端迭代器”(off-the-end iterator)。 表明它指向了一个不存在的元素。如果 vector 为空,begin 返回的迭代器与 end 返回的迭代器相同。

2.2 容器类型成员

类型别名成员:

  • iterator
  • const_iterator
  • size_type无符号整型,足以存储此容器类型的最大可能容器长度
  • difference_type 足够保存两个迭代器之间的距离
  • value_type元素类型
  • reference元素的 左值类型,与value_type&含义相同
  • const_reference元素的常量左值类型,等效于 const value_type&

反向迭代器的额外成员:

  • reverse_iterator
  • const_reverse_iterator
  • c.rbegin()、c.rend() 返回逆序迭代器
  • c.crbegin()、c.crend()返回常量逆序迭代器

begin和end成员,r开头方法获取反向迭代器,c开头方法获取常量迭代器

2.3 容器定义和初始化

除array,每个容器都有默认构造方法

  • C c;默认构造函数(array除外)
  • c c1(c2);拷贝c2中的元素到c1
  • C c(b , e);构造c,将迭代器b和e指定范围的元素拷贝到c(array不支持)
  • C c{a, b, c...};列表初始化

除上面操作外,顺序容器的初始化方式还有:

  • C seq(n) seq包含了n个元素,这些元素进行值初始化
  • C seq(n, t) seq包含了n个初始化值为t的元素

为了使程序更清晰、简短,容器类型最常用的构造函数是默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行时性能,并且使容器更容易使用。

将一个容器初始化为另一个容器的拷贝

将一个容器初始化为另一个容器的拷贝有两种方法:

  • 直接拷贝整个容器
  • 拷贝一个由迭代器指定的范围(array除外)
list<string>  list1 = {"a", "b", "c"};
vector<const  char*>  vector1 = {"a", "b", "c"};

deque<string> list2(list1);//错误,容器类型不匹配
vector<string>  vector2(vector1);//错误,元素类型必须匹配

forward_list<string> list3(vector1.begin(), vector1.end());//正确,const  char*可以转换为string

标准array

array<int, 10> ia1;//正确的类型,元素执行默认初始化
array<int, 10> ia2;
array<int, 10> ia3 = {42};//第一个元素42,其余0
array<int> ia4;//错误,array<int>不是一个类型

//相同类型的array间允许赋值
ia1 = ia2;

2.4 容器的赋值与swap

赋值:

  • c1 = c2;删除容器 c1 的所有元素,然后将 c2 的元素复制给 c1。c1 和 c2 的类型(包括容器类型和元素类型)必须相同
  • c1 = {a, b, c...};初始化列表
  • a.swap(b);
  • swap(a, b);交换内容:调用完该函数后,c1 中存放的是 c2 原来的元素, c2 中存放的则是 c1 原来的元素。c1 和 c2 的类型必须相同。该函数的执行速度通常要比将 c2 复制到 c1 的操作快

顺序容器assign(不适用于关联容器和array):

  • c.assign(b,e) 重新设置 c 的元素:将迭代器 b 和 e 标记的范围内所有的元素复制到 c 中。b 和 e 必须不是指向 c 中元素的迭代器
  • c.assign(n,t) 将容器 c 重新设置为存储 n 个值为 t 的元素
  • c.assign(il) 将容器 c 中存储的元素替换为初始化列表il中的元素

注意:

  • 使用非成员版本的swap是一个好习惯
  • 赋值和 assign 操作使左操作数容器的所有迭代器、引用、指针失效
  • swap 操作则不会使迭代器失效。完成 swap 操作后,尽管被交换的元素已经存放在另一容器中,但迭代器仍然指向相同的元素。(array和string除外)
  • 除array外,swap 只是交换了两个容器的内部数据结构
  • 除array外,swap不对任何元素进行拷贝、删除、插入操作,可以保证在常数时间内完成

2.5 容器大小操作

  • c.size() 返回容器 c 中的元素个数。返回类型为 c::size_type
  • c.max_size() 返回容器 c 可容纳的最多元素个数,返回类型为 c::size_type
  • c.empty() 返回标记容器大小是否为 0 的布尔值
  • c.resize(n) 调整容器 c 的长度大小,使其能容纳 n 个元素,如果 n < c.size(),则删除多出来的元素;否则,添加采用值初始化的新元素
  • c.resize(n,t) 调整容器 c 的长度大小,使其能容纳 n 个元素。所有新添加的元素值都为 t

注意:

  • resize不适用于 array
  • 如果resize缩小容器,则执行被删除元素的迭代器、引用、指针都会失效,对vector、string、deque进行resize可能导致迭代器、引用、指针失效

2.6 关系运算符

所有的容器类型都支持用关系操作符来实现两个容器的比较。但比较的容器必须具有相同的容器类型,而且其元素类型也必须相同。 容器的比较是基于容器内元素的比较。容器的比较使用了元素类型定义的同一个关系操作符:两个容器做!=比较使用了其元素类型定义的!=操作符。 如果容器的元素类型不支持某种操作符,则该容器就不能做这种比较运算。

  • 如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器就相等;否则,它们就不相等。
  • 如果两个容器的长度不相同,但较短的容器中所有元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
  • 如果两个容器都不是对方的初始子序列,则它们的比较结果取决于所比较的第一个不相等的元素。

注意:

  • ==、!=所有容器都支持
  • <、<= 、>、>=无序关联容器不支持

3 顺序容器操作

3.1 在顺序容器中添加元素(array除外)

  • c.push_back(t) 在容器 c 的前端添加值为 t 的元素。返回 void 类型
  • c.push_front(t) 在容器 c 的尾部添加值为 t 的元素。返回 void 类型。只适用于 list 和 deque 容器类型
  • c.insert(p, t)、c.emplace(p, t); 在迭代器 p 所指向的元素前面插入值为 t 的新元素。返回指向新添加元素的迭代器
  • c.insert(p, n, t) 在迭代器 p 所指向的元素前面插入 n 个值为 t 的新元素。返回 void 类型
  • c.insert(p, b, e) 在迭代器 p 所指向的元素前面插入由迭代器 b 和 e 标记的范围内的元素。返回 void 类型

3.2 访问元素

  • c.back() 返回容器 c 的最后一个元素的引用。如果 c 为空,则该操作未定义
  • c.front() 返回容器 c 的第一个元素的引用。如果 c 为空,则该操作未定义
  • c[n] 返回下标为 n 的元素的引用。如果 n < 0 或 n >= c.size(),则该操作未定义。只适用于 vector 和 deque 容器
  • c.at(n) 返回下标为 n 的元素的引用。如果下标越界,则该操作未定义。只适用于 vector 和 deque 容器

访问成员函数返回的是引用

auto &v = c.back();//接收引用,如果容器不是const的,则返回的是普通引用
auto v = c.back();//用返回值初始化v

下表操作与安全的随机访问

vector<string> vec;//空vector
cout << vev[0];//运行时错误,ves中没有元素
cout << vec.at(0);//抛出一个out_ot_range异常

3.3 删除元素

  • c.erase(p) 删除迭代器 p 所指向的元素。返回一个迭代器,它指向被删除元素后面的元素。如果 p 指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置。如果 p 本身就是指向超出末端的下一位置的迭代器,则该函数未定义。
  • c.erase(b, e) 删除迭代器 b 和 e 所标记的范围内所有的元素。返回一个迭代器,它指向被删除元素段后面的元素。如果 e 本身就是指向超出末端的下一位置的迭代器,则返回的迭代器也指向容器的超出末端的下一位置。
  • c.clear() 删除容器 c 内的所有元素。返回 void。
  • c.pop_back() 删除容器 c 的最后一个元素。返回 void。如果 c 为空容器,则该函数未定义。
  • c.pop_front() 删除容器 c 的第一个元素。返回 void。如果 c 为空容器,则该函数未定义。只适用于 list 或 deque 容器

3.4 forward_list操作

由于forward_list是单向链表,定义了专门针对forward_list操作的函数。

3.5 容器操作可能使迭代器失效

向容器中添加元素或从容器中删除元素都有可能会使容器的指针、引用、或迭代器失效,一个失效的指针、引用、或迭代器不再代表任何元素, 使用一个失效的指针、引用、或迭代器是严重的程序设计错误,注意以下操作:

向容器中添加元素后:

  • 如果容器是vector或string,而且存储控件被重新分配,则指向容器的迭代器、指针和引用都会失效,如果存储空间没有被重新分配,执行插入位置之前的 元素的迭代器、指针和引用还有效,但指向插入位置之后元素的容器的迭代器、指针和引用都会失效
  • 对于deque,插入到首尾位置之外的任何位置都会使迭代器、指针和引用失效,如果在首尾位置添加元素,则迭代器会失效,而执行存在元素的引用和指针不会失效
  • 对于list和forward_list,指向容器的迭代器、指针和引用都不仍有效

当我们从容器中删除元素后,指向这些删除元素的迭代器、指针和引用都会失效,这很容易理解,除此之外,当我们删除一个元素后:

  • 对于list和forward_list,指向容器其他位置的迭代器、指针和引用都仍有效
  • 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素之外其他元素的迭代器、指针和引用也会失效,如果删除deque的尾元素,则尾后迭代器 也会失效,其他迭代器、指针和引用不受引用,如果删除首原生,则迭代器、指针和引用不受引用
  • 对于vector和string,指向被删除元素之前的迭代器、指针和引用仍有效,当我们删除元素后,尾后迭代器总是会失效。

4 容器如何增长

为了支持快速查找,vector存储的数据是连续存放的,当插入一个数据时,如果现有的连续内存不足够,那么会重新申请一块连续的内存, 把数据所有元素重新拷贝到新的连续内存中,再进行插入插入,为了避免频繁的执行这种操作,重新申请内存块时vector会多申请一些预留控件, 用于之后可能的插入操作。

管理容器大小的方法:

  • c.shrink_to_fit()将capacity减少为size()修改的大小,仅适用于vector、string、deque
  • c.capacity()不重新分配空间,c可以保存多少元素,只适用于vector、string
  • c.reserve(n)分配至少能够容纳n个元素的空间,只适用于vector、string

5 string操作

string 类型支持长度可变的字符串,C++ 标准库将负责管理与存储字符相关的内存,以及提供各种有用的操作。标准库 string 类型的目的就是满足对字符串的一般应用。

5.1 定义和初始化

  • string s1 默认构造函数 s1 为空串
  • string s2(s1) 将 s2 初始化为 s1 的一个副本
  • string s3("value") 将 s3 初始化为一个字符串字面值副本
  • string s4(n, 'c') 将 s4 初始化为字符 ‘c’ 的 n 个副本

因为历史原因以及为了与 C 语言兼容,字符串字面值与标准库 string 类型不是同一种类型。 这一点很容易引起混乱,编程时一定要注意区分字符串字面值和 string 数据类型的使用,这很重要。

5.2 读写

使用标准输入输出操作符来读写 string 对象

读入未知数目的 string 对象

int main() {
  string word;
  while (cin >> word)
    cout << word << endl;
  return 0;
}

使用getline ()读取整行文本,getline函数接受两个参数:一个输入流对象和一个 string 对象。 getline 函数从输入流的下一行读取,并保存读取的内容到不包括换行符。和输入操作符不一样的是, getline 并不忽略行开头的换行符。只要 getline 遇到换行符,即便它是输入的第一个字符,getline 也将停止读入并返回。 如果第一个字符就是换行符,则 string 参数将被置为空 string。

int main() {
  string line;
  while (getline(cin, line))
    cout << line << endl;
  return 0;
}

5.3 操作

关系操作符

  • v1 == v2 比较 v1 与 v2 的内容,相等则返回 true,否则返回 false
  • !=, <, <=, >, >= 保持这些操作符惯有的含义

substr 操作

使用 substr 操作可在指定 string 对象中检索需要的子串:

  • s.substr(pos, n) 返回一个 string 类型的字符串,它包含 s 中从下标 pos 开始的 n 个字符
  • s.substr(pos) 返回一个 string 类型的字符串,它包含从下标 pos 开始到 s 末尾的所有字符
  • s.substr() 返回 s 的副本

append 和 replace 操作

  • s.append(args) 将 args 串接在 s 后面。返回 s 引用
  • s.replace(pos, len, args) 删除 s 中从下标 pos 开始的 len 个字符,用 args 指定的字符替换之。返回 s 的引用。
  • s.replace(b, e, args) 删除迭代器 b 和 e 标记范围内所有的字符,用 args 替换之。返回 s 的引用。

查找操作

string 类提供了多种查找函数,每种函数以不同形式的 find 命名。这些操作全都返回 string::size_type 类型的值, 以下标形式标记查找匹配所发生的位置;或者返回一个名为 string::npos 的特殊值,说明查找没有匹配。string 类将 npos 定义为保证大于任何有效下标的值:

  • s.find(args) 在 s 中查找 args 的第一次出现
  • s.rfind(args) 在 s 中查找 args 的最后一次出现
  • s.find_first_of(args) 在 s 中查找 args 的任意字符的第一次出现
  • s.find_last_of(args) 在 s 中查找 args 的任意字符的最后一次出现
  • s.find_first_not_of(args) 在 s 中查找第一个不属于 args 的字符
  • s.find_last_not_of(args) 在 s 中查找最后一个不属于 args 的字符

compare 操作

除了关系操作符,string 类型还提供了一组 compare 操作,用于实现字典顺序的比较。这些操作的结果类似于 C 语言中的库函数 strcmp,

  • s.compare(s2) 比较 s 和 s2
  • s.compare(pos1, n1, s2) 让 s 中从 pos 下标位置开始的 n1 个字符与 s2 做比较
  • s.compare(pos1, n1, s2, pos2, n2) 让 s 中从 pos1 下标位置开始的 n1 个字符与 s2 中从 pos2 下标位置开始的 n2 个字符做比较
  • s.compare(cp) 比较 s 和 cp 所指向的以空字符结束的字符串
  • s.compare(pos1, n1, cp) 让 s 中从 pos1 下标位置开始的 n1 个字符与 cp 所指向的字符串做比较
  • s.compare(pos1, n1, cp, n2) 让 s 中从 pos1 下标位置开始的 n1 个字符与 cp 所指向的字符串的前 n2 个字符做比较

compare 函数返回下面列出的三种可能值之一:

  • 正数,此时 s1 大于 args 所代表的 string 对象
  • 负数,此时 s1 小于 args 所代表的 string 对象
  • 0,此时 s1 恰好等于 args 所代表的 string 对象

数值转换操作

  • to_string(val) 返回val值的string形式
  • stoi(s, p, b) 返回s的起始字串(表示整数内容)的数值,返回值是int类型,b表示转换所用基数,默认为10,p是size_t指针,用来保存s中第一个非数组字符的下标,默认为0
  • stol(s, p, b) 同上,返回值是long类型
  • stoul(s, p, b) 同上,返回值是无符号long类型
  • stoll(s, p, b) 同上,返回值是long long类型
  • stoull(s, p, b) 同上,返回值是无符号long long类型
  • stof(s, p, b) 返回s的起始字串(表示浮点内容)的数值,返回值是float类型
  • stod(s, p, b) 同上,返回值是double类型
  • stold(s, p, b) 同上,返回值是long double类型

此外string 类型还支持大多数顺序容器操作。


6 容器适配器

除了顺序容器,标准库还提供了三种顺序容器适配器:queue、priority_queue 和 stack。

适配器(adaptor)是标准库中通用的概念,包括容器适配器、迭代器适配器和函数适配器。本质上,适配器是使一事物的行为类似于另一事物的行为的一种机制。 容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。例如,stack(栈)适配器可使任何一种顺序容器以栈的方式工作。

适配器基本操作:

  • size_type 一种类型,足以存储此适配器类型最大对象的长度
  • value_type 元素类型
  • container_type基础容器的类型,适配器在此容器类型上实现
  • A a; 创建一个新空适配器,命名为 a
  • A a(c); 创建一个名为 a 的新适配器,初始化为容器 c 的副本
  • 关系操作符:所有适配器都支持全部关系操作符: ==、 !=、 <、 <=、>、>=

6.1 适配器的初始化

所有适配器都定义了两个构造函数:默认构造函数用于创建空对象,而带一个容器参数的构造函数将参数容器的副本作为其基础值。 例如,假设 deq 是 deque<int> 类型的容器,则可用 deq 初始化一个新的栈:

stack<int> stk(deq);

6.2 覆盖基础容器类型

默认的 stack 和 queue 都基于 deque 容器实现,而 priority_queue 则在 vector 容器上实现。在创建适配器时, 通过将一个顺序容器指定为适配器的第二个类型实参,可覆盖其关联的基础容器类型:

//在vector之上实现的空栈。
stack< string, vector<string> > str_stk;
//stk 2是在vector上实现的,并持有svec的副本。
stack<string, vector<string> > str_stk2(svec);

使用适配器要注意两点:

  • 对于给定的适配器,其关联的容器必须满足一定的约束条件。stack 适配器所关联的基础容器可以是任意一种顺序容器类型。 因此, stack 栈可以建立在 vector、list 或者 deque 容器之上。而 queue 适配器要求其关联的基础容器必须提供 push\_front 运算, 因此只能建立在 list 容器上,而不能建立在 vector 容器上。priority_queue 适配器要求提供随机访问功能,因此可建立在 vector 或 deque 容器上,但不能建立在 list 容器上。
  • 应该选择使用适配器的操作而不是所基础容器的操作。例如,尽管栈是以 deque 容器为基础实现的,但是程序员不能直接访问deque 所提供的操作。 如不能在栈上调用 push_back 函数,而是必须使用栈所提供的名为 push 的操作。

6.3 适配器的运算

栈容器适配器支持的操作:

  • s.empty() 如果栈为空,则返回 true,否则返回返回栈中元素的个数
  • s.size() 返回栈中元素的个数
  • s.pop() 删除栈顶元素的值,但不返回其值
  • s.top() 返回栈顶元素的值,但不删除该元素
  • s.push(item) 在栈顶压入新元素

队列和优先级队列支持的操作:

  • q.empty() 如果队列为空,则返回 true,否则返回 false
  • q.size() 返回队列中元素的个数
  • q.pop() 删除队首元素,但不返回其值
  • q.front() 返回队首元素的值,但不删除该元素该操作只适用于队列
  • q.back() 返回队尾元素的值,但不删除该元素该操作只适用于队列
  • q.top() 返回具有最高优先级的元素值,但不删除该元素该操作只适用于优先级队列
  • q.push(item) 对于 queue,在队尾压入一个新元素,对于 priority_quue,在基于优先级的适当位置插入新元素