我们之前讲过了vector时基于顺序表的连续空间来实现的,下面我们讲解一下的deque和list。
一、deque 容器详解
1. deque 的核心特性
deque(double-ended queue,双端队列)是分段连续存储的序列容器,它结合了 vector 和 list 的部分优点:
- 两端高效增删:头部和尾部插入 / 删除元素的时间复杂度均为 O (1)
- 随机访问 :支持下标运算符
[]和at()方法,时间复杂度 O (1) - 分段内存结构:由多个固定大小的内存块组成,通过中控器(map)管理这些块
- 无预留空间概念:不需要像 vector 那样预先分配大块连续内存
- 迭代器是随机访问迭代器 :支持所有随机访问操作(
+、-、+=、-=、[]等)
2. 头文件与命名空间
cpp
#include <deque> // 必须包含此头文件
using namespace std; // 或使用 std::deque 前缀
3. deque 的定义与初始化
cpp
// 1. 空deque
deque<int> d1;
// 2. 包含n个默认值的deque
deque<int> d2(5); // 5个0
// 3. 包含n个指定值的deque
deque<int> d3(5, 10); // 5个10
// 4. 拷贝构造
deque<int> d4(d3);
// 5. 迭代器范围构造
int arr[] = {1, 2, 3, 4, 5};
deque<int> d5(arr, arr + 5);
// 6. 列表初始化(C++11及以上)
deque<int> d6 = {1, 2, 3, 4, 5};
deque<int> d7{1, 2, 3, 4, 5};
4. 元素访问
cpp
deque<int> d = {1, 2, 3, 4, 5};
// 1. 下标运算符[](不检查越界)
cout << d[2] << endl; // 输出3
// 2. at()方法(检查越界,抛出out_of_range异常)
cout << d.at(2) << endl; // 输出3
// 3. 访问首尾元素
cout << d.front() << endl; // 输出1
cout << d.back() << endl; // 输出5
// 4. 底层数据指针(不常用,因为内存不连续)
// int* p = d.data(); // 错误!deque没有data()方法
5. 元素增删操作
(1)两端增删(O (1) 时间复杂度)
cpp
deque<int> d;
// 尾部添加
d.push_back(10); // d: [10]
d.push_back(20); // d: [10, 20]
// 头部添加
d.push_front(5); // d: [5, 10, 20]
d.push_front(0); // d: [0, 5, 10, 20]
// 尾部删除
d.pop_back(); // d: [0, 5, 10]
// 头部删除
d.pop_front(); // d: [5, 10]
(2)指定位置插入(O (n) 时间复杂度)
cpp
deque<int> d = {1, 2, 3, 4, 5};
// 在指定位置插入单个元素
d.insert(d.begin() + 2, 10); // d: [1, 2, 10, 3, 4, 5]
// 在指定位置插入n个相同元素
d.insert(d.end(), 3, 20); // d: [1, 2, 10, 3, 4, 5, 20, 20, 20]
// 在指定位置插入迭代器范围的元素
int arr[] = {30, 40};
d.insert(d.begin(), arr, arr + 2); // d: [30, 40, 1, 2, 10, 3, 4, 5, 20, 20, 20]
(3)删除元素(O (n) 时间复杂度)
cpp
deque<int> d = {1, 2, 3, 4, 5, 6, 7, 8};
// 删除指定位置的元素
d.erase(d.begin() + 3); // d: [1, 2, 3, 5, 6, 7, 8]
// 删除迭代器范围的元素
d.erase(d.begin() + 1, d.begin() + 4); // d: [1, 6, 7, 8]
// 清空所有元素
d.clear(); // d: 空
6. 容量管理
deque 没有 reserve() 和 capacity() 方法,因为它不需要预先分配大块连续内存:
cpp
deque<int> d = {1, 2, 3, 4, 5};
// 获取元素个数
cout << d.size() << endl; // 输出5
// 判断是否为空
cout << d.empty() << endl; // 输出0(false)
// 获取deque能容纳的最大元素个数
cout << d.max_size() << endl;
// 调整大小
d.resize(3); // d: [1, 2, 3]
d.resize(5, 10); // d: [1, 2, 3, 10, 10]
// 释放未使用的内存(C++11及以上)
d.shrink_to_fit();
7. 迭代器与遍历
cpp
deque<int> d = {1, 2, 3, 4, 5};
// 1. 普通for循环(下标访问)
for (int i = 0; i < d.size(); ++i) {
cout << d[i] << " ";
}
cout << endl;
// 2. 迭代器遍历
for (deque<int>::iterator it = d.begin(); it != d.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 3. 范围for循环(C++11及以上)
for (int num : d) {
cout << num << " ";
}
cout << endl;
// 4. 反向迭代器
for (deque<int>::reverse_iterator rit = d.rbegin(); rit != d.rend(); ++rit) {
cout << *rit << " ";
}
cout << endl;
8. 与 STL 算法结合
deque 的随机访问迭代器可以与所有 STL 算法配合使用:
cpp
#include <algorithm>
deque<int> d = {5, 2, 8, 1, 9, 3};
// 排序
sort(d.begin(), d.end()); // d: [1, 2, 3, 5, 8, 9]
// 反转
reverse(d.begin(), d.end()); // d: [9, 8, 5, 3, 2, 1]
// 查找
deque<int>::iterator it = find(d.begin(), d.end(), 5);
if (it != d.end()) {
cout << "找到元素5,位置:" << it - d.begin() << endl;
}
// 计数
int count_3 = count(d.begin(), d.end(), 3);
cout << "元素3出现的次数:" << count_3 << endl;
二、deque 常见问题与注意事项
1. 迭代器失效问题
deque 的迭代器失效情况比 vector 复杂:
- 两端增删 :
push_back()、push_front()、pop_back()、pop_front()会使所有迭代器失效 - 中间插入 / 删除 :会使所有迭代器失效
- resize() :会使所有迭代器失效
注意:与 vector 不同,deque 即使只在尾部添加元素,也可能导致迭代器失效,因为可能需要分配新的内存块并更新中控器。
2. 内存结构的特殊性
- deque 的内存不是连续的,而是由多个固定大小的块组成
- 因此不能使用
data()方法获取底层数组指针 - 元素的地址可能不连续,不能通过指针算术运算访问元素
- 随机访问的效率略低于 vector,因为需要计算元素所在的块和块内偏移
3. 与 vector 的性能差异
- 两端增删:deque O (1),vector 尾部 O (1)、头部 O (n)
- 随机访问:vector O (1) 更快,deque O (1) 稍慢
- 中间插入 / 删除:两者都是 O (n),但 deque 通常更快(因为不需要移动大量连续内存)
三、deque 综合示例
cpp
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std;
int main() {
// 创建并初始化deque
deque<int> d = {3, 1, 4, 1, 5, 9, 2, 6};
// 两端增删
d.push_front(0);
d.push_back(7);
cout << "两端增删后:";
for (int num : d) cout << num << " ";
cout << endl;
// 中间插入
d.insert(d.begin() + 5, 8);
cout << "中间插入后:";
for (int num : d) cout << num << " ";
cout << endl;
// 排序
sort(d.begin(), d.end());
cout << "排序后:";
for (int num : d) cout << num << " ";
cout << endl;
// 删除重复元素
auto last = unique(d.begin(), d.end());
d.erase(last, d.end());
cout << "去重后:";
for (int num : d) cout << num << " ";
cout << endl;
return 0;
}
输出:
cpp
两端增删后:0 3 1 4 1 5 9 2 6 7
中间插入后:0 3 1 4 1 8 5 9 2 6 7
排序后:0 1 1 2 3 4 5 6 7 8 9
去重后:0 1 2 3 4 5 6 7 8 9
四、list 容器详解
1. list 的核心特性
list 是双向链表实现的序列容器:
- 任意位置高效增删:在任何位置插入 / 删除元素的时间复杂度均为 O (1)
- 不支持随机访问 :不能使用下标运算符
[]和at()方法 - 内存不连续:每个元素都有自己的内存空间,通过指针连接
- 迭代器是双向迭代器 :只支持
++和--操作,不支持+、-、+=、-=等随机访问操作 - 空间开销大:每个元素需要额外存储前后两个指针
2. 头文件与命名空间
cpp
#include <list> // 必须包含此头文件
using namespace std; // 或使用 std::list 前缀
3. list 的定义与初始化
cpp
// 1. 空list
list<int> l1;
// 2. 包含n个默认值的list
list<int> l2(5); // 5个0
// 3. 包含n个指定值的list
list<int> l3(5, 10); // 5个10
// 4. 拷贝构造
list<int> l4(l3);
// 5. 迭代器范围构造
int arr[] = {1, 2, 3, 4, 5};
list<int> l5(arr, arr + 5);
// 6. 列表初始化(C++11及以上)
list<int> l6 = {1, 2, 3, 4, 5};
list<int> l7{1, 2, 3, 4, 5};
4. 元素访问
list 不支持随机访问,只能通过迭代器或首尾元素访问:
cpp
list<int> l = {1, 2, 3, 4, 5};
// 访问首尾元素
cout << l.front() << endl; // 输出1
cout << l.back() << endl; // 输出5
// 访问中间元素(必须通过迭代器遍历)
list<int>::iterator it = l.begin();
advance(it, 2); // 移动迭代器到第3个元素(O(n)时间复杂度)
cout << *it << endl; // 输出3
5. 元素增删操作
(1)两端增删(O (1) 时间复杂度)
cpp
list<int> l;
// 尾部添加
l.push_back(10); // l: [10]
l.push_back(20); // l: [10, 20]
// 头部添加
l.push_front(5); // l: [5, 10, 20]
l.push_front(0); // l: [0, 5, 10, 20]
// 尾部删除
l.pop_back(); // l: [0, 5, 10]
// 头部删除
l.pop_front(); // l: [5, 10]
(2)指定位置插入(O (1) 时间复杂度)
cpp
list<int> l = {1, 2, 3, 4, 5};
list<int>::iterator it = l.begin();
advance(it, 2); // 指向元素3
// 在指定位置插入单个元素
l.insert(it, 10); // l: [1, 2, 10, 3, 4, 5]
// 在指定位置插入n个相同元素
l.insert(l.end(), 3, 20); // l: [1, 2, 10, 3, 4, 5, 20, 20, 20]
// 在指定位置插入迭代器范围的元素
int arr[] = {30, 40};
l.insert(l.begin(), arr, arr + 2); // l: [30, 40, 1, 2, 10, 3, 4, 5, 20, 20, 20]
(3)删除元素(O (1) 时间复杂度,已知迭代器位置)
cpp
list<int> l = {1, 2, 3, 4, 5, 6, 7, 8};
list<int>::iterator it = l.begin();
advance(it, 3); // 指向元素4
// 删除指定位置的元素
l.erase(it); // l: [1, 2, 3, 5, 6, 7, 8]
// 删除迭代器范围的元素
it = l.begin();
advance(it, 1);
l.erase(it, l.begin() + 4); // 错误!list迭代器不支持+操作
// 正确写法:
list<int>::iterator it2 = l.begin();
advance(it2, 4);
l.erase(it, it2); // l: [1, 6, 7, 8]
// 删除所有值为x的元素
l.remove(6); // l: [1, 7, 8]
// 清空所有元素
l.clear(); // l: 空
6. 容量管理
list 没有 reserve()、capacity() 和 shrink_to_fit() 方法,因为它的内存是按需分配的:
cpp
list<int> l = {1, 2, 3, 4, 5};
// 获取元素个数
cout << l.size() << endl; // 输出5
// 判断是否为空
cout << l.empty() << endl; // 输出0(false)
// 获取list能容纳的最大元素个数
cout << l.max_size() << endl;
// 调整大小
l.resize(3); // l: [1, 2, 3]
l.resize(5, 10); // l: [1, 2, 3, 10, 10]
7. 迭代器与遍历
list 的迭代器是双向迭代器,只能使用 ++ 和 -- 操作:
cpp
list<int> l = {1, 2, 3, 4, 5};
// 1. 迭代器遍历
for (list<int>::iterator it = l.begin(); it != l.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 2. 范围for循环(C++11及以上)
for (int num : l) {
cout << num << " ";
}
cout << endl;
// 3. 反向迭代器
for (list<int>::reverse_iterator rit = l.rbegin(); rit != l.rend(); ++rit) {
cout << *rit << " ";
}
cout << endl;
// 错误示例:不能使用下标访问
// for (int i = 0; i < l.size(); ++i) {
// cout << l[i] << " "; // 编译错误
// }
8. 与 STL 算法结合
list 的双向迭代器不能与需要随机访问迭代器的算法配合使用(如 sort()、binary_search() 等)。为此,list 提供了自己的成员函数版本:
cpp
list<int> l = {5, 2, 8, 1, 9, 3};
// list自己的排序算法(比通用sort更高效)
l.sort(); // l: [1, 2, 3, 5, 8, 9]
// 降序排序
l.sort(greater<int>()); // l: [9, 8, 5, 3, 2, 1]
// 反转
l.reverse(); // l: [1, 2, 3, 5, 8, 9]
// 合并两个有序list
list<int> l2 = {4, 6, 7};
l.merge(l2); // l: [1, 2, 3, 4, 5, 6, 7, 8, 9], l2: 空
// 删除重复元素(必须先排序)
l.unique();
// 移除满足条件的元素
l.remove_if([](int x) { return x % 2 == 0; }); // 移除所有偶数
五、list 常见问题与注意事项
1. 迭代器失效问题
list 的迭代器失效情况非常少:
- 插入操作:不会使任何迭代器失效
- 删除操作:只会使指向被删除元素的迭代器失效,其他迭代器仍然有效
- resize():只会使指向被删除元素的迭代器失效
这是 list 最大的优势之一,在频繁插入和删除元素的场景下,不需要担心迭代器失效问题。
2. 不支持随机访问的影响
- 不能使用下标运算符
[]和at()方法 - 不能使用
it + n这样的迭代器算术运算 - 访问中间元素需要 O (n) 时间复杂度
- 不能使用需要随机访问迭代器的 STL 算法(如
sort()、binary_search()、nth_element()等)
3. 空间开销
- 每个元素需要额外存储两个指针(前向指针和后向指针)
- 在 64 位系统中,每个元素的额外开销是 16 字节
- 对于小数据类型(如 char、int),空间开销非常大
六、list 综合示例
cpp
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
int main() {
// 创建并初始化list
list<int> l1 = {5, 2, 9, 1, 5, 6};
list<int> l2 = {3, 7, 4, 8};
// 排序
l1.sort();
l2.sort();
cout << "排序后l1:";
for (int num : l1) cout << num << " ";
cout << endl;
cout << "排序后l2:";
for (int num : l2) cout << num << " ";
cout << endl;
// 合并
l1.merge(l2);
cout << "合并后l1:";
for (int num : l1) cout << num << " ";
cout << endl;
// 去重
l1.unique();
cout << "去重后l1:";
for (int num : l1) cout << num << " ";
cout << endl;
// 反转
l1.reverse();
cout << "反转后l1:";
for (int num : l1) cout << num << " ";
cout << endl;
// 删除偶数
l1.remove_if([](int x) { return x % 2 == 0; });
cout << "删除偶数后l1:";
for (int num : l1) cout << num << " ";
cout << endl;
return 0;
}
输出:
排序后l1:1 2 5 5 6 9
排序后l2:3 4 7 8
合并后l1:1 2 3 4 5 5 6 7 8 9
去重后l1:1 2 3 4 5 6 7 8 9
反转后l1:9 8 7 6 5 4 3 2 1
删除偶数后l1:9 7 5 3 1
七、vector、deque、list 对比
1. 核心特性对比表
表格
| 特性 | vector | deque | list |
|---|---|---|---|
| 底层实现 | 动态数组 | 分段动态数组 | 双向链表 |
| 内存连续性 | 连续 | 分段连续 | 不连续 |
| 随机访问 | 支持(O (1),最快) | 支持(O (1),稍慢) | 不支持(O (n)) |
| 头部增删 | O(n) | O(1) | O(1) |
| 尾部增删 | O (1)(均摊) | O(1) | O(1) |
| 中间增删 | O(n) | O(n) | O (1)(已知迭代器) |
| 迭代器类型 | 随机访问迭代器 | 随机访问迭代器 | 双向迭代器 |
| 迭代器失效 | 增删可能导致所有迭代器失效 | 任何增删都导致所有迭代器失效 | 仅删除使被删元素迭代器失效 |
| 空间开销 | 小(仅元素本身) | 中等(中控器 + 内存块) | 大(每个元素两个指针) |
| 缓存友好性 | 最好 | 较好 | 最差 |
2. 适用场景对比
vector 适用场景
- 需要频繁随机访问元素
- 主要在尾部进行增删操作
- 对内存使用效率要求高
- 需要与 C 风格数组兼容(通过
data()方法)
deque 适用场景
- 需要在两端频繁进行增删操作
- 需要随机访问元素
- 不希望像 vector 那样在扩容时移动大量元素
- 作为 stack 和 queue 的默认底层容器
list 适用场景
- 需要在任意位置频繁进行插入和删除操作
- 不需要随机访问元素
- 对迭代器失效要求严格(插入不失效,删除仅失效被删元素)
- 元素大小较大,移动成本高
3. 性能对比总结
- 随机访问性能:vector > deque > list
- 两端增删性能:deque ≈ list > vector(头部)
- 中间增删性能:list > deque > vector
- 遍历性能:vector > deque > list(缓存友好性差异)
- 内存使用效率:vector > deque > list
八、选择建议
- 优先使用 vector:除非有明确的理由使用其他容器
- 如果需要在两端频繁增删:使用 deque
- 如果需要在中间频繁增删:使用 list
- 如果需要排序 :优先使用 vector 或 deque(通用
sort()算法比 list 的成员函数sort()更快) - 如果需要频繁合并容器 :使用 list(
merge()操作是 O (1) 时间复杂度)