以下是 C++ 中 std::deque
(双端队列)的详细解释,涵盖其底层实现、核心特性、常用操作及适用场景:
1. 基本概念
定义:
std::deque
(Double-Ended Queue)是一种双端动态数组,支持在头部和尾部高效插入/删除元素。头文件:
#include <deque>
核心特性:
两端操作高效:头部和尾部插入/删除的时间复杂度为 O(1)。
支持随机访问:通过下标直接访问元素(类似
vector
)。分段连续存储:元素分布在多个固定大小的内存块中,通过映射表管理(非完全连续,但逻辑上连续)。
2. 底层实现
分段存储结构:
deque
由多个内存块(chunks)组成,每个块存储部分元素。通过一个中央控制器(通常是数组或
vector
)管理这些块的地址。内存分布示例:
[块1] → [块2] → [块3]
元素逻辑上连续,但物理上可能分散在不同内存块中。
扩展机制:
当头部或尾部空间不足时,动态分配新内存块。
无需像
vector
那样整体搬迁数据,两端插入效率更高。
3. 初始化与构造
(1) 空容器
std::deque<int> dq; // 空deque
(2) 指定大小和初始值
std::deque<int> dq1(5); // 5个元素,默认初始化为0 std::deque<int> dq2(5, 42); // 5个元素,初始化为42
(3) 列表初始化(C++11+)
std::deque<int> dq = {1, 2, 3, 4, 5}; std::deque<std::string> names {"Alice", "Bob"};
(4) 从其他容器构造
std::vector<int> vec {10, 20, 30}; std::deque<int> dq3(vec.begin(), vec.end()); // 复制vec的元素 std::deque<int> dq4(dq3); // 拷贝构造
4. 元素访问
(1) 随机访问(下标)
int val1 = dq[2]; // 不检查越界(类似数组) int val2 = dq.at(3); // 越界抛出std::out_of_range异常
(2) 首尾元素
int first = dq.front(); // 首元素 int last = dq.back(); // 尾元素
(3) 迭代器遍历
// 正向遍历 for (auto it = dq.begin(); it != dq.end(); ++it) { std::cout << *it << " "; } // 范围for循环(C++11+) for (const auto& num : dq) { std::cout << num << " "; }
5. 添加/删除元素
(1) 两端操作
dq.push_front(0); // 头部插入元素 dq.emplace_front(-1); // 更高效(C++11+,避免拷贝) dq.push_back(6); // 尾部插入元素 dq.emplace_back(7); // 直接构造元素 dq.pop_front(); // 删除头部元素 dq.pop_back(); // 删除尾部元素
(2) 任意位置插入/删除
auto it = dq.begin() + 2; dq.insert(it, 99); // 在位置2插入99(时间复杂度 O(n)) dq.erase(it); // 删除位置2的元素(时间复杂度 O(n))
6. 容量管理
int size = dq.size(); // 元素数量 bool isEmpty = dq.empty(); // 是否为空 dq.resize(10); // 调整大小,多出元素默认初始化 dq.resize(15, 42); // 调整到15个元素,新增元素初始化为42
7. 性能分析
(1) 时间复杂度
操作 | 时间复杂度 |
---|---|
头部插入/删除(push_front /pop_front ) | O(1) |
尾部插入/删除(push_back /pop_back ) | O(1) |
中间插入/删除(insert /erase ) | O(n) |
随机访问(operator[] ) | O(1)(但比 vector 慢) |
(2) 与 vector
对比
特性 | std::deque | std::vector |
---|---|---|
内存布局 | 分段连续 | 完全连续 |
头部插入效率 | O(1) | O(n)(需移动所有元素) |
随机访问速度 | 稍慢(需计算块和偏移) | 极快 |
扩容机制 | 动态添加内存块 | 整体搬迁数据 |
迭代器失效 | 插入可能使所有迭代器失效 | 扩容时全部失效 |
8. 适用场景
高频两端操作:如实现队列(FIFO)或双端队列。
需随机访问但无法预知大小:避免
vector
扩容时的性能损失。避免中间插入/删除:与
vector
类似,中间操作效率低。
9. 示例代码
#include <deque> #include <iostream> int main() { std::deque<int> dq {2, 3, 4}; dq.push_front(1); // 头部插入1 → [1, 2, 3, 4] dq.emplace_back(5); // 尾部插入5 → [1, 2, 3, 4, 5] // 删除中间元素(效率低) auto it = dq.begin() + 2; dq.erase(it); // 删除3 → [1, 2, 4, 5] // 随机访问 std::cout << dq[1] << std::endl; // 输出2 // 遍历输出 for (int num : dq) { std::cout << num << " "; // 输出:1 2 4 5 } return 0; }
10. 注意事项
迭代器失效:
在中间插入/删除元素会导致所有迭代器失效。
在两端插入通常不会使其他位置的迭代器失效(除非触发内存块重新分配)。
内存碎片:分段存储可能导致内存碎片,影响缓存局部性。
性能取舍:若需要高频中间操作,优先选择
std::list
;若需要连续内存,选择std::vector
。
11. 总结
优势:高效的两端操作 + 随机访问。
劣势:中间操作效率低,内存非完全连续。
设计哲学:在
vector
和list
之间权衡,适用于特定场景(如滑动窗口算法、任务调度等)。
系统当前共有 438 篇文章