文章目录
一、deque 的基本概念
deque(双端队列,全称 double-ended queue)是 C++ STL 中的一种序列式容器,它允许在头部和尾部高效地进行插入和删除操作。与 vector 相比,deque 在头部插入/删除的时间复杂度为 O(1),而 vector 为 O(n)。
1.1 deque 的主要特性
- 双端操作:支持在头部和尾部高效插入/删除
- 随机访问:支持通过下标直接访问元素(O(1) 时间复杂度)
- 动态扩容:自动管理内存,根据需要动态增长
- 非连续存储:元素并非存储在单一连续内存块中
二、deque 的内部实现原理
2.1 deque 的数据结构
deque 通常采用"分段连续"的存储方式,具体实现为一个"中央控制器"(map)和多个缓冲区(buffer):
template <class T>
class deque {
private:
T** map; // 指向指针数组的指针(中央控制器)
size_t map_size; // map 的大小
iterator start; // 指向第一个元素的迭代器
iterator finish; // 指向最后一个元素的下一个位置的迭代器
// ...
};
2.2 deque 的迭代器设计
deque 的迭代器比 vector 的迭代器复杂得多,因为它需要跨越不同的缓冲区:
template <class T>
struct __deque_iterator {
T* cur; // 当前元素指针
T* first; // 当前缓冲区的起始位置
T* last; // 当前缓冲区的结束位置
T** node; // 指向中央控制器中当前缓冲区指针的位置
// ...
};
2.3 deque 的内存布局
deque 的内存布局可以形象地表示为:
中央控制器 (map)
+---+---+---+---+---+---+---+
| * | * | * | * | * | * | * |
+---+---+---+---+---+---+---+
| | | | | | |
v v v v v v v
+---+ +---+ +---+ +---+ +---+
|B1| |B2| |B3| |B4| |B5| ...
+---+ +---+ +---+ +---+ +---+
每个缓冲区(B1, B2等)存储实际元素,中央控制器中的指针指向这些缓冲区。
三、deque 的核心操作实现
3.1 插入操作
头部插入(push_front)
void push_front(const value_type& value) {
if (start.cur != start.first) {
// 当前缓冲区还有空间
construct(start.cur - 1, value);
--start.cur;
} else {
// 需要分配新缓冲区
reserve_map_at_front();
*(start.node - 1) = allocate_node();
start.set_node(start.node - 1);
start.cur = start.last - 1;
construct(start.cur, value);
}
}
尾部插入(push_back)
void push_back(const value_type& value) {
if (finish.cur != finish.last - 1) {
// 当前缓冲区还有空间
construct(finish.cur, value);
++finish.cur;
} else {
// 需要分配新缓冲区
reserve_map_at_back();
*(finish.node + 1) = allocate_node();
finish.set_node(finish.node + 1);
finish.cur = finish.first;
construct(finish.cur, value);
++finish.cur;
}
}
3.2 删除操作
头部删除(pop_front)
void pop_front() {
if (start.cur != start.last - 1) {
// 当前缓冲区还有元素
destroy(start.cur);
++start.cur;
} else {
// 需要切换到前一个缓冲区
destroy(start.cur);
deallocate_node(start.first);
start.set_node(start.node + 1);
start.cur = start.first;
}
}
尾部删除(pop_back)
void pop_back() {
if (finish.cur != finish.first) {
// 当前缓冲区还有元素
--finish.cur;
destroy(finish.cur);
} else {
// 需要切换到前一个缓冲区
deallocate_node(finish.first);
finish.set_node(finish.node - 1);
finish.cur = finish.last - 1;
destroy(finish.cur);
}
}
3.3 随机访问
reference operator[](size_type n) {
return start[difference_type(n)];
}
reference front() { return *start; }
reference back() {
iterator tmp = finish;
--tmp;
return *tmp;
}
四、deque 的扩容机制
4.1 中央控制器扩容
当中央控制器的空间不足时,deque 会重新分配一个更大的中央控制器:
void reserve_map_at_back(size_type nodes_to_add = 1) {
if (nodes_to_add + 1 > map_size - (finish.node - map))
reallocate_map(nodes_to_add, false);
}
void reserve_map_at_front(size_type nodes_to_add = 1) {
if (nodes_to_add > start.node - map)
reallocate_map(nodes_to_add, true);
}
4.2 缓冲区分配
每个缓冲区的大小通常是固定的,不同实现可能有不同策略。例如,GCC 的实现中:
inline size_t __deque_buf_size(size_t size) {
return size < 512 ? size_t(512 / size) : size_t(1);
}
五、deque 与 vector 的对比
特性 | deque | vector |
---|---|---|
内部结构 | 分段连续存储 | 单一连续内存块 |
头部插入/删除 | O(1) | O(n) |
尾部插入/删除 | O(1) | 平摊 O(1) |
中间插入/删除 | O(n) | O(n) |
随机访问 | O(1) | O(1) |
迭代器失效 | 插入/删除可能导致失效 | 插入/删除通常导致失效 |
内存使用 | 更高(有额外指针开销) | 更低 |
缓存局部性 | 较差 | 优秀 |
六、deque 的应用示例
6.1 基本操作示例
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq;
// 头部和尾部插入
dq.push_back(1);
dq.push_front(2);
dq.push_back(3);
dq.push_front(4);
// 输出: 4 2 1 3
for (int num : dq) {
std::cout << num << " ";
}
std::cout << std::endl;
// 随机访问
std::cout << "Element at index 2: " << dq[2] << std::endl;
// 头部和尾部删除
dq.pop_front();
dq.pop_back();
// 输出: 2 1
for (int num : dq) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
6.2 性能测试示例
#include <iostream>
#include <deque>
#include <vector>
#include <chrono>
const int TEST_SIZE = 1000000;
void test_deque() {
std::deque<int> dq;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TEST_SIZE; ++i) {
dq.push_front(i);
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "deque push_front time: " << diff.count() << " s\n";
}
void test_vector() {
std::vector<int> vec;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TEST_SIZE; ++i) {
vec.insert(vec.begin(), i);
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "vector insert at begin time: " << diff.count() << " s\n";
}
int main() {
test_deque();
test_vector();
return 0;
}
七、deque 的迭代器失效问题
deque 的迭代器失效规则比 vector 复杂:
-
插入操作:
- 在头部或尾部插入:通常不会使任何迭代器失效
- 在中间插入:会使所有迭代器失效
-
删除操作:
- 在头部或尾部删除:通常只使指向被删除元素的迭代器失效
- 在中间删除:会使所有迭代器失效
-
扩容操作:
- 中央控制器扩容会使所有迭代器失效
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq = {1, 2, 3, 4, 5};
auto it = dq.begin() + 2; // 指向3
dq.push_back(6); // 不影响迭代器
std::cout << *it << std::endl; // 输出3
dq.push_front(0); // 不影响迭代器
std::cout << *it << std::endl; // 输出3
dq.insert(dq.begin() + 1, 9); // 使所有迭代器失效
// std::cout << *it << std::endl; // 未定义行为
return 0;
}
八、deque 的优缺点分析
8.1 优点
- 高效的双端操作:在头部和尾部插入/删除都是 O(1) 时间复杂度
- 动态扩容:不需要预先指定大小
- 随机访问:支持通过下标快速访问元素
- 内存效率:相比 vector,扩容时不需要移动所有元素
8.2 缺点
- 内存不连续:缓存局部性不如 vector
- 内存开销:需要维护中央控制器和多个缓冲区
- 中间操作性能:在中间插入/删除效率不高
- 迭代器失效:规则复杂,容易出错
九、deque 的最佳实践
-
适合场景:
- 需要频繁在头部和尾部插入/删除
- 需要随机访问但不需要最高性能
- 不确定元素数量但需要高效两端操作
-
避免场景:
- 需要最高性能的随机访问
- 需要内存连续性的场景(如与C API交互)
- 需要频繁在中间位置插入/删除
-
性能优化:
- 如果知道大致元素数量,可以使用构造函数预分配空间
std::deque<int> dq(1000); // 预分配空间
- 避免不必要的中间插入/删除
- 考虑使用 vector 如果只需要尾部操作
十、总结
deque 是 C++ STL 中一个功能强大的双端队列容器,它通过分段连续的存储方式实现了高效的双端操作和随机访问能力。理解 deque 的内部实现机制对于正确使用和优化性能至关重要。虽然 deque 在某些方面不如 vector 高效,但在需要频繁双端操作的场景下,它是无可替代的选择。
通过本文的详细分析,读者应该能够:
- 理解 deque 的内部实现原理
- 掌握 deque 的核心操作及其时间复杂度
- 了解 deque 与 vector 的差异及适用场景
- 避免常见的迭代器失效问题
- 在实际开发中合理选择和使用 deque