deque(double-ended queue,双端队列)是STL中一种重要的序列容器。与vector相比,deque在头部和尾部的插入删除操作具有常数时间复杂度优势。本文将深入探讨deque的内部实现机制、使用场景以及与vector的性能对比。

一、deque的基本特性

1. 什么是deque

deque是一种双端队列容器,支持在常数时间内对两端进行插入和删除操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <deque>
#include <iostream>

int main() {
std::deque<int> dq;

// 尾部操作
dq.push_back(1);
dq.push_back(2);
dq.push_back(3);

// 头部操作
dq.push_front(0);
dq.push_front(-1);

// 支持随机访问
std::cout << "Element at index 2: " << dq[2] << std::endl;

// 遍历
for (const auto& elem : dq) {
std::cout << elem << " ";
}
std::cout << std::endl;
// Output: -1 0 1 2 3

return 0;
}

2. deque的核心特性

特性 说明
双端操作 push_front、push_back、pop_front、pop_back均为O(1)
随机访问 支持[]操作符和at(),为O(1)
迭代器 双向迭代器
内存管理 多段连续内存块,通过中控器管理
插入删除 两端操作为O(1),中间操作为O(n)

二、deque的内部实现机制

1. 分段连续内存结构

deque的内部结构由多个固定大小的连续内存块(称为buffer)组成,通过一个map(指向这些buffer指针的数组)进行管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// deque内部结构示意
template<typename T>
class deque {
private:
// map:管理所有buffer的指针数组
T** map_;
size_t map_size_; // map的容量
size_t num_elements_; // 元素总数

// 迭代器相关信息
size_t block_size_; // 每个buffer的大小(通常为512字节)

// 逻辑起始位置(在第一个buffer中的偏移)
size_t start_index_;

// 逻辑结束位置
size_t end_index_;
};

2. 中控器(Map)

deque使用一个"中控器"来管理多个内存块:

1
2
3
4
5
6
7
// deque内存布局示意
// map[0] map[1] map[2] map[3]
// ↓ ↓ ↓ ↓
// 逻辑结构: [-1] [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
// ↑ ↑
// start_index=1 end_index=3
// (buffer[0]中) (buffer[3]中)

3. 迭代器实现

deque的迭代器需要处理跨块访问的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// deque迭代器内部结构
template<typename T>
struct deque_iterator {
T** cur; // 指向当前元素的指针
T** first; // 指向当前buffer的起始
T** last; // 指向当前buffer的末尾(+1)

// 前进到下一个元素
void increment() {
++cur;
if (cur == last) { // 到达当前buffer末尾
// 移动到下一个buffer
first = *(++T** (cur = first));
last = first + block_size;
}
}

// 后退到上一个元素
void decrement() {
if (cur == first) { // 到达当前buffer开头
// 移动到上一个buffer
last = *(--T** (cur = last));
first = last - block_size;
}
--cur;
}
};

4. 内存重分配策略

当deque在头部或尾部扩展时,会分配新的buffer并更新中控器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// push_back时的重分配逻辑
void push_back(const T& value) {
if (end_index_ < block_size_ - 1) {
// 当前buffer还有空间
construct_at(buffer_[last_buffer_] + end_index_, value);
++end_index_;
} else {
// 需要分配新的buffer
if (map_size_ < num_buffers_ + 1) {
// map空间不足,需要扩容
reallocate_map();
}
// 在末尾添加新的buffer
map_[num_buffers_] = allocate_buffer();
construct_at(map_[num_buffers_], value);
end_index_ = 1;
++num_buffers_;
}
++num_elements_;
}

三、deque与vector对比

1. 底层结构对比

特性 deque vector
内存布局 多段连续 + 中控器 单段连续
扩展方式 两端可扩展 尾部扩展
内存预留 不需要预留 通常需要reserve()
迭代器类型 双向迭代器 随机访问迭代器
数据局部性 较差 较好

2. 性能对比测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <deque>
#include <vector>
#include <chrono>
#include <iostream>

const int N = 1000000;

void benchmark(const std::string& name, auto&& container, auto&& operation) {
auto start = std::chrono::high_resolution_clock::now();
operation();
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << name << ": " << duration.count() << " μs" << std::endl;
}

int main() {
// 尾部插入对比
benchmark("deque push_back", std::deque<int>{}, []() {
std::deque<int> dq;
for (int i = 0; i < N; ++i) {
dq.push_back(i);
}
});

benchmark("vector push_back", std::vector<int>{}, []() {
std::vector<int> vec;
vec.reserve(N); // 预留空间
for (int i = 0; i < N; ++i) {
vec.push_back(i);
}
});

// 头部插入对比
benchmark("deque push_front", std::deque<int>{}, []() {
std::deque<int> dq;
for (int i = 0; i < N / 10; ++i) { // 减少迭代次数
dq.push_front(i);
}
});

benchmark("vector insert(begin)", std::vector<int>{}, []() {
std::vector<int> vec;
for (int i = 0; i < N / 10; ++i) {
vec.insert(vec.begin(), i); // 非常慢
}
});

return 0;
}

3. 典型场景选择建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 场景1:需要频繁在头部插入删除
void scenario1() {
// deque是更好的选择
std::deque<int> dq;
dq.push_front(1); // O(1)
dq.push_back(2); // O(1)
}

// 场景2:主要在尾部操作,需要缓存友好
void scenario2() {
// vector是更好的选择
std::vector<int> vec;
vec.reserve(1000);
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
}

// 场景3:需要两端操作且需要随机访问
void scenario3() {
// deque是更好的选择
std::deque<int> dq;
dq.push_back(1);
dq.push_front(0);
dq.push_back(2);
// 可以随机访问
std::cout << dq[1] << std::endl; // O(1)
}

四、deque的成员函数详解

1. 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <deque>

int main() {
// 默认构造函数
std::deque<int> dq1;

// 指定大小和初始值
std::deque<int> dq2(10); // 10个元素,默认值0
std::deque<int> dq3(10, 5); // 10个元素,值都是5

// 迭代器范围构造
std::vector<int> vec = {1, 2, 3, 4, 5};
std::deque<int> dq4(vec.begin(), vec.end());

// 拷贝构造
std::deque<int> dq5(dq4);

// 移动构造
std::deque<int> dq6(std::move(dq5));

// 初始化列表
std::deque<int> dq7 = {1, 2, 3, 4, 5};

return 0;
}

2. 元素访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <deque>
#include <stdexcept>

int main() {
std::deque<int> dq = {10, 20, 30, 40, 50};

// operator[] 不进行边界检查
std::cout << dq[2] << std::endl; // 30
std::cout << dq.at(2) << std::endl; // 30

// at()进行边界检查,超出范围抛出out_of_range
try {
std::cout << dq.at(10) << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << std::endl;
}

// 访问首尾元素
std::cout << dq.front() << std::endl; // 10
std::cout << dq.back() << std::endl; // 50

return 0;
}

3. 迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <deque>

int main() {
std::deque<int> dq = {1, 2, 3, 4, 5};

// 正向迭代器
for (auto it = dq.begin(); it != dq.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

// 反向迭代器
for (auto rit = dq.rbegin(); rit != dq.rend(); ++rit) {
std::cout << *rit << " ";
}
std::cout << std::endl;

// const迭代器
for (auto cit = dq.cbegin(); cit != dq.cend(); ++cit) {
std::cout << *cit << " ";
}
std::cout << std::endl;

// 使用范围for循环(C++11)
for (const auto& elem : dq) {
std::cout << elem << " ";
}
std::cout << std::endl;

return 0;
}

4. 容量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <deque>

int main() {
std::deque<int> dq;

// 空容器判断
std::cout << std::boolalpha << dq.empty() << std::endl; // true

// 添加元素
for (int i = 0; i < 100; ++i) {
dq.push_back(i);
}

// 元素数量
std::cout << dq.size() << std::endl; // 100

// 最大容量
std::cout << dq.max_size() << std::endl;

// 调整大小
dq.resize(50); // 截断到50个元素
dq.resize(200, -1); // 扩展到200,填充值-1

// 收缩到刚好容纳元素
dq.shrink_to_fit();

return 0;
}

5. 修改器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <deque>

int main() {
std::deque<int> dq = {1, 2, 3, 4, 5};

// 赋值
dq.assign(5, 10); // 5个元素,都是10
dq.assign({1, 2, 3}); // 从初始化列表赋值

// 交换
std::deque<int> dq2 = {100, 200};
dq.swap(dq2); // 交换内容

// 清空
dq.clear(); // 清空所有元素

// 插入
dq.insert(dq.begin() + 2, 99); // 在位置2插入99
dq.insert(dq.end(), 3, 88); // 在末尾插入3个88
dq.insert(dq.begin(), vec.begin(), vec.end()); // 范围插入

// 删除
dq.erase(dq.begin()); // 删除第一个元素
dq.erase(dq.begin(), dq.begin() + 3); // 删除范围

// 两端操作
dq.push_back(6);
dq.pop_back();
dq.push_front(0);
dq.pop_front();

// emplace(C++11)
dq.emplace_back(7); // 原位构造
dq.emplace_front(-1);
dq.emplace(dq.begin() + 3, 50);

return 0;
}

五、实战应用场景

1. 实现滑动窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <deque>
#include <vector>
#include <iostream>

std::vector<int> slidingWindowMax(const std::vector<int>& nums, int k) {
std::deque<int> dq; // 存储索引
std::vector<int> result;

for (size_t i = 0; i < nums.size(); ++i) {
// 移除超出窗口范围的元素
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}

// 维护单调递减队列
while (!dq.empty() && nums[dq.back()] <= nums[i]) {
dq.pop_back();
}
dq.push_back(i);

// 记录窗口最大值
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}

return result;
}

int main() {
std::vector<int> nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;

auto result = slidingWindowMax(nums, k);
for (int val : result) {
std::cout << val << " ";
}
std::cout << std::endl;
// Output: 3 3 5 5 6 7

return 0;
}

2. 实现LRU缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <deque>
#include <unordered_map>
#include <iostream>

template<typename K, typename V>
class LRUCache {
private:
size_t capacity_;
std::deque<K> recent_; // 记录使用顺序
std::unordered_map<K, V> cache_;
std::unordered_map<K, typename std::deque<K>::iterator> pos_;

public:
explicit LRUCache(size_t capacity) : capacity_(capacity) {}

bool get(const K& key, V& value) {
auto it = cache_.find(key);
if (it == cache_.end()) {
return false;
}
value = it->second;
// 移动到front(最近使用)
recent_.erase(pos_[key]);
recent_.push_front(key);
pos_[key] = recent_.begin();
return true;
}

void put(const K& key, const V& value) {
if (cache_.find(key) != cache_.end()) {
// 更新现有key
cache_[key] = value;
recent_.erase(pos_[key]);
recent_.push_front(key);
pos_[key] = recent_.begin();
} else {
// 添加新key
if (cache_.size() >= capacity_) {
// 淘汰最久未使用的
K lruKey = recent_.back();
cache_.erase(lruKey);
pos_.erase(lruKey);
recent_.pop_back();
}
cache_[key] = value;
recent_.push_front(key);
pos_[key] = recent_.begin();
}
}

void print() const {
std::cout << "Cache state (oldest -> newest): ";
for (const auto& key : recent_) {
std::cout << key << ":" << cache_.at(key) << " ";
}
std::cout << std::endl;
}
};

int main() {
LRUCache<int, int> cache(3);

cache.put(1, 100);
cache.put(2, 200);
cache.put(3, 300);
cache.print(); // Cache state: 3:300 2:200 1:100

int value;
cache.get(2, value);
std::cout << "Key 2 value: " << value << std::endl;
cache.print(); // Cache state: 2:200 3:300 1:100

cache.put(4, 400);
cache.print(); // Cache state: 4:400 2:200 3:300 (key 1 evicted)

return 0;
}

3. 字符串编辑历史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <deque>
#include <string>
#include <iostream>
#include <sstream>

class TextEditor {
private:
std::deque<std::string> history_; // 编辑历史
size_t current_pos_; // 当前版本位置

public:
TextEditor() : current_pos_(0) {}

void edit(const std::string& text) {
// 如果在中间位置编辑,清除后面的历史
if (current_pos_ < history_.size()) {
history_.erase(history_.begin() + current_pos_, history_.end());
}
history_.push_back(text);
++current_pos_;
}

std::string undo() {
if (current_pos_ == 0) {
return "";
}
--current_pos_;
return history_[current_pos_];
}

std::string redo() {
if (current_pos_ >= history_.size()) {
return "";
}
return history_[current_pos_++];
}

void printHistory() const {
std::cout << "History (current=" << current_pos_ << "): ";
for (size_t i = 0; i < history_.size(); ++i) {
if (i == current_pos_) {
std::cout << "[" << history_[i] << "] ";
} else {
std::cout << history_[i] << " ";
}
}
std::cout << std::endl;
}
};

int main() {
TextEditor editor;

editor.edit("Hello");
editor.edit("Hello World");
editor.edit("Hello World!");
editor.printHistory();
// History: Hello Hello World Hello World![3]

std::cout << "Undo: " << editor.undo() << std::endl;
editor.printHistory();
// History: Hello Hello World[2] Hello World!

std::cout << "Undo: " << editor.undo() << std::endl;
editor.printHistory();
// History: Hello[1] Hello World Hello World!

std::cout << "Redo: " << editor.redo() << std::endl;
editor.printHistory();
// History: Hello Hello World[2] Hello World!

return 0;
}

4. 生产者消费者队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <deque>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
#include <chrono>

template<typename T>
class ThreadSafeQueue {
private:
std::deque<T> queue_;
std::mutex mtx_;
std::condition_variable cv_;
bool closed_ = false;

public:
void push(T value) {
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push_back(std::move(value));
}
cv_.notify_one();
}

bool pop(T& value) {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return !queue_.empty() || closed_; });

if (queue_.empty()) {
return false;
}

value = std::move(queue_.front());
queue_.pop_front();
return true;
}

void close() {
{
std::lock_guard<std::mutex> lock(mtx_);
closed_ = true;
}
cv_.notify_all();
}

bool empty() const {
std::lock_guard<std::mutex> lock(mtx_);
return queue_.empty();
}

size_t size() const {
std::lock_guard<std::mutex> lock(mtx_);
return queue_.size();
}
};

int main() {
ThreadSafeQueue<int> queue;

// 生产者线程
std::thread producer([&queue]() {
for (int i = 1; i <= 10; ++i) {
queue.push(i);
std::cout << "Produced: " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
queue.close();
});

// 消费者线程
std::thread consumer([&queue]() {
int value;
while (queue.pop(value)) {
std::cout << "Consumed: " << value << std::endl;
}
});

producer.join();
consumer.join();

return 0;
}

六、deque使用注意事项

1. 迭代器失效规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <deque>

int main() {
std::deque<int> dq = {1, 2, 3, 4, 5};

auto it = dq.begin() + 2; // 指向元素3

// push_back和push_front不会使迭代器失效(除了可能重分配的迭代器)
dq.push_back(6);
dq.push_front(0);

// 注意:deque的迭代器失效规则与vector不同
// 在deque中间插入/删除元素会导致所有迭代器失效

dq.insert(it, 99); // 在中间插入,所有迭代器可能失效

// erase会使被擦除元素之后的迭代器失效
auto it2 = dq.begin() + 3;
dq.erase(it2); // it2及之后的迭代器失效

return 0;
}

2. 内存使用考量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <deque>

int main() {
std::deque<int> dq;

// deque不会一次性分配所有内存
for (int i = 0; i < 1000000; ++i) {
dq.push_back(i);
}

// deque的内存可能比vector更分散
// 但不会像vector那样因扩容而复制所有元素

// 如果需要紧凑内存,考虑使用vector
// 如果需要两端的O(1)操作,deque是更好的选择

return 0;
}

3. 性能优化建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <deque>

int main() {
std::deque<int> dq;

// 预先设置大小可以减少内存重分配
dq.resize(1000000); // 预先分配空间

// 但要注意resize不会改变deque的容量特性
// deque仍然会在两端操作时分配新的buffer

// 使用emplace系列函数避免不必要的拷贝
struct Heavy {
std::string data;
Heavy(const std::string& s) : data(s) {}
};

std::deque<Heavy> hd;
// 不好:需要拷贝
hd.push_back(Heavy("test"));

// 好:原地构造
hd.emplace_back("test");

return 0;
}

七、总结

deque作为STL中重要的序列容器,提供了vector和list所不具备的优势:

核心要点

  1. 双端操作优势:在头部和尾部的插入删除都是O(1)
  2. 分段连续内存:通过中控器管理多个内存块,避免整体复制
  3. 随机访问支持:虽然迭代器不是真正随机访问,但支持下标操作
  4. 迭代器失效:两端操作不会使迭代器失效,中间操作会使迭代器失效

选择建议

  • 需要频繁在两端操作:使用deque
  • 主要在尾部操作,追求缓存命中:使用vector
  • 需要在中间频繁插入删除:使用list
  • 需要在两端操作且需要频繁在中间插入删除:综合考虑或使用其他数据结构

理解deque的内部机制对于正确使用和优化C++程序至关重要,希望本文能帮助你更好地掌握这一重要容器。