STL之deque和list以及两者与vector的对比

我们之前讲过了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

八、选择建议

  1. 优先使用 vector:除非有明确的理由使用其他容器
  2. 如果需要在两端频繁增删:使用 deque
  3. 如果需要在中间频繁增删:使用 list
  4. 如果需要排序 :优先使用 vector 或 deque(通用 sort() 算法比 list 的成员函数 sort() 更快)
  5. 如果需要频繁合并容器 :使用 list(merge() 操作是 O (1) 时间复杂度)
相关推荐
凤山老林1 小时前
DDD(领域驱动设计)在复杂业务系统中的落地指南
java·开发语言·数据库·ddd·领域驱动
郝学胜_神的一滴1 小时前
CMake 012:Linux 下动态库与可执行程序的单文件构建
c++·cmake
凯瑟琳.奥古斯特1 小时前
子查询原理与实战案例解析
开发语言·数据库·职场和发展·数据库开发
小poop1 小时前
操作符详解:从入门到精通
c++
Eiceblue1 小时前
Python 操作 Excel:数据分组、分类汇总与取消分组全解
开发语言·python·excel
山上三树2 小时前
C/C++ 高频报错速查表(开发通用版)
c语言·开发语言·c++
Tian_Hang2 小时前
Factory Method | 工厂方法
开发语言·c++
wearegogog1232 小时前
基于MATLAB实现雷达RCS Swerling模型
开发语言·matlab
星梦清河2 小时前
Java—异步编程
java·开发语言