Chapter9 顺序容器
1.概述
容器 | 特性 |
---|---|
vector | 变长数组,随机访问,尾部插入和删除 |
deque | 双端队列,随机访问,头尾的插入删除 |
list | 双向链表,双向顺序访问,任何位置插入删除 |
forward_list | 单向链表,单向顺序访问,任何位置插入删除 |
array | 定长数组 |
string | 与vector相似,用于保存字符 |
选择容器的依据
- vector是较为一般的选择
- list和forward_list的额外空间开销较大,不适合小元素
- 只有vector、deque、array能随机访问
- 如果开始要中间插入,后面要随机访问,可以考虑先尾端插入再sort
- 如果无法sort解决,可以先用list输入,结束后拷贝到vector进行随机访问
- 实在不行只能比较vector和list的相对开销
2.基本操作
2.1 容器共有操作
forward_list<int> d;
vector<int> c;
// list<int> c;
// deque<int> c;
// string c;
// array<int, 10> a; //必须固定大小,但是可以赋值
auto it1 = c.begin();
//迭代器,++向大方向自增(forward_list不支持--);如果容器是const的,也会返回const迭代器
auto it2 = c.cbegin(); //const 迭代器
auto it3 = c.rbegin(); //反向迭代器,++向小方向自增
auto it4 = c.crbegin(); //反向const迭代器
auto size = c.size(); //容器大小,单向链表不支持
auto diff = c.end() - c.begin(); //迭代器间距离
auto emtpy = c.empty(); //是否为空
auto t = (c.begin() == c.end()) == c.empty(); //等价的两种表述
for (auto it = c.begin(); it != c.end(); it++); //遍历元素
for (auto it = c.cbegin(); it != c.cend(); it++); //只读的遍历
2.2 随机访问
int n;
vector<int> c;
c.back(); //返回尾元素的引用
c.front(); //返回首元素的引用
c.at(n); //返回对应下标元素的引用
auto &i = c.back(); //赋值给引用变量
2.3 初始化和赋值
vector<int> v1(c); //用另一个同类对象初始化
vector<int> v2(10); //初始化空间大小
swap(v1, v2);
//常数时间交换(array除外),只是交换了控制信息,迭代器指针等不会失效(string除外)
vector<int> v3(10, 1); //指定值初始化
v3.assign(10, 1); //赋值
vector<int> v4(d.begin(), d.end()); //用其他容器初始化
v4.assign(d.begin(), d.end()); //用其他容器赋值,不能用自己迭代器赋值,因为迭代器会失效
vector<int> v5{10};
vector<int> v6 = {10};
vector<int> v7 = c;
3.插入
3.1 push_back()
除了 array 和forward_list 外均支持
插入的是对象的拷贝,除非使用的是右值引用
vecotr<string> v;
string word;
while(cin >> word)
v.push_back(word); //拷贝插入
v.push_back("123"); //右值引用插入
push_front() 同理
3.2 insert()
vector<int> c;
deque<int> q;
q.insert(q.begin(), 1); //插入到对应迭代器之前
q.insert(q.end(), 10, 1); //插入多个元素
q.insert(q.end(), {1, 1, 1, 1}); //初始化列表插入
q.insert(q.end(), c.begin(), c.end()); //不能插入自己的迭代器,因为会失效
auto it = q.end(); //insert 均返回第一个插入元素位置的迭代器
int n;
while (cin >> n)
it = q.insert(it, n);//利用返回值更新迭代器,反复在同一个位置插入
3.3 emplace()
emplace和push不同,是构造而不是拷贝临时元素
直接调用对应类型的构造函数,适合自定义的元素较为复杂的容器
push会创建临时对象然后拷贝,emplace直接调用构造函数,参数与构造函数匹配
vector<vector<int>> v; //下面两句等价
v.push_back({1,1,1}); //内置类型可以push列表初始化,右值引用移动语义
v.emplace_back(3,1); //自定义类型最好用emplace
4.删除
删除的过程中迭代器会失效:
deque所有迭代器都会失效
vecetor、string被删除元素后面的迭代器会失效
因此需要及时更新迭代器
4.1 erase()
deque<int> q;
q.pop_back(); //返回void
q.pop_front(); //返回void
q.erase(q.begin()); //返回后面一个元素的迭代器
q.erase(q.begin(),q.end()); //返回最后一个元素后面一个元素的迭代器
q.clear(); //删除所有,返回void
//删除特定元素
vector<int> v{1,1,1};
auto it = v.begin();
while(it != v.end())
{
if(*it == 2)
it = v.erase(it);//必须更新迭代器
else
++it;
}
4.2 forward_list
forward_list 比较特殊,因为单向链表中的添加和删除都需要对前一个元素进行修改
vector<int> v;
forward_list<int> f;
f.before_begin(); //首前迭代器,方便插入删除
f.erase_after(f.before_begin()); //删除第一个元素
f.erase_after(f.before_begin(), f.end()); //删除范围
f.insert_after(f.before_begin(), 1); //在首部插入元素
f.insert_after(f.begin(), 1); //在第二个位置插入元素
f.insert_after(f.before_begin(), v.begin(), v.end());
f.emplace_after(f.before_begin(), 1);
//链表中插入删除元素需要维护两个迭代器
auto pre = f.before_begin();
auto cur = f.begin();
while(cur != f.end())
{
if(*cur == 2)
cur = f.erase_after(pre);
else
{
++cur;
++pre;
}
}
5. 改变容器大小
array不支持
vector<int> v(10);
v.resize(20);
v.resize(30,1);//新添加进的元素全部初始化为1
6. 迭代器失效
6.1 插入
容器 | 操作 |
---|---|
vector / string | 如果存储空间重新分配,所有迭代器、指针、引用都会失效 如果空间没有重新分配,插入位置之后的迭代器、指针、引用会失效 |
deque | 除首尾位置外任何位置,所有迭代器、指针、引用都会失效 如果在首尾插入,迭代器会失效,指针和引用不会失效(说明地址没有变) |
list / forward_list | 迭代器、指针、引用仍有效 |
6.2 删除
容器 | 操作 |
---|---|
vector / string | 删除元素之后的均失效 |
deque | 除首尾位置外任何位置,所有迭代器、指针、引用都会失效 如果在删除尾元素,尾后迭代器会失效,其他不受影响 如果删除首元素,不受影响 |
list / forward_list | 除删除位置外的迭代器、指针、引用仍有效 |
因此如果用尾后迭代器作循环的判断条件,不能保存该迭代器,必须每次调用end()
下面是典型错误,在循环过程中更新了begin但是没有更新end
vector<int> v{1,2,3,4,5,6);
auto begin = v.begin(), end = v.end();
while(begin != end)
{
++begin(); //向前移动,使得插入元素在begin后面
begin = v.inset(begin(),42);//已失效更新begin迭代器
++begin(); //跳过刚插入的元素
}
7.vector内存管理
每次分配内存的时候都会多分配(adaptor)一些内存区域,但是并没有构建(construct)。在push_back()的过程中逐步构建新元素,如果现有的内存空间已满,不够用来构建,就将所有内容一定到一个新分配的更大的内存空间
内存管理函数:
v.size(); //已经存在的元素的数量
//适用于vector、string、deque:
v.shrink_to_fit(n); //将capacity减少到size大小
//适用于vector、string:
v.capacity(); //返回已经分配的内存中可容纳元素的数量
v.reserve(n); //分配能够容纳n个元素的内存空间
已分配空间用完时,标准库的实现可能是申请加倍的空间
8.string
8.1 构造
除了之前介绍的共有构造函数外,string还支持:
string s(cp,n); //字符数组cp前n个字符
string s(s2,pos2); //从string s2的第pos2个字符开始copy构造
string s(s2,pos2,len2) //从string s2的第pos2个字符开始len2个字符
string s2 = s.substr(len1,len2); //返回一个子串的copy
8.2 insert/erase
string提供接受下标版本的insert和erase
s.insert(s.size(), 5, '!'); //末端插入5个字符
s.erase(s.size() - 5, 5); //删除末端5个字符
接受C风格字符串的赋值和插入
s.assign(cp, 7); //赋值为cp指针开始的7个字符
s.insert(s.size(), cp + 7); //末端插入cp+7指针指向位置后面的字符
8.3 append/replace
s.append("..."); //在末端直接插入对应串
s.replace(pos, len, "..."); //从pos开始的len个字符替换为对应串(不需要等长)
//字符串部分都可以用其他构造函数替代
8.4 搜索
auto pos = s.find("..."); //返回第一个匹配位置的下标,未找到返回nops(-1)
auto pos = s.rfind("..."); //返回最后一个匹配位置的下标,未找到返回nops(-1)
auto pos = s.find_first_of("..."); //返回与模式中任何一个字符相等的第一个位置
auto pos = s.find_first_not_of("..."); //与上面相反
auto pos = s.find_last_of("..."); //返回与模式中任何一个字符相等的最后一个位置
auto pos = s.find_last_not_of("..."); //与上面相反
//后面参数还可以加上s起始查找位置
找到每个出现的位置:
string numbers = "...";
string::size_type pos = 0;
while((pos = s.find(numbers, pos) != string::npos)
{
cout << pos << endl;//找到下一个对应串
++pos;
}
8.5 数值转换
int n = 15;
string s = to_string(n); //数字转换成字符串
double d = stod(s,p,b); //字符串转换成数字,p是开始转换的子串位置,默认0,b是进制,默认10
//还有 stoi(),stol()等
9.适配器(adaptor)
包括stackqueuepriority_queue三种
默认情况下,前两种基于deque实现,后面基于vector实现