C++ STL deque容器详解

1.引言

C++ 标准模板库(STL)中,deque(双端队列)是一种非常重要且灵活的序列式容器。与vector类似,它支持快速的随机访问,并且在头尾两端都能高效地插入和删除元素。

1.1什么是 deque

deque 是 "double-ended queue" 的缩写,翻译为"双端队列"。它是一种可以在容器头部和尾部快速进行插入和删除操作的顺序容器。

1.2dequevector 的异同

特性 deque vector
内存结构 分段连续(块+映射表) 完全连续
头部插入/删除 O(1) O(n)(需要移动所有元素)
尾部插入/删除 O(1) O(1)(均摊)
中间插入/删除 O(n) O(n)
随机访问性能 O(1),稍慢于vector O(1),最优
内存稳定性 可能导致realloc 较稳定(不连续)
迭代器失效 头尾操作可能局部失效,中间操作全失效 扩容时全失效

关键区别

deque 适合高频头尾操作(如任务队列、滑动窗口)。

vector 适合尾部操作+随机访问(如动态数组、数据缓存)。

1.3deque 的适用场景和优势

适用场景

任务调度系统:需频繁从队列头部取出任务,尾部添加新任务。

滑动窗口算法:如 LeetCode 题目中的双指针问题(高效移除/添加两端元素)。

撤销操作栈:支持从历史记录的前后两端操作(如 Ctrl+Z 和 Ctrl+Y)。

缓存实现:结合 push_frontpop_back 实现 LRU 缓存淘汰策略。

优势总结

灵活性:双端操作无需移动大量元素。

平衡的性能:随机访问接近 vector,头尾操作优于 vector

内存效率:分段存储避免 vector 扩容时的全量拷贝。

2. deque 的常见构造方式

2.1默认构造

创建一个空的 deque,不分配内存,直到首次插入元素时才分配初始块。

deque<int> dq1;// 空 deque,size=0,capacity=0

适用场景:

元素数量未知,后续动态添加。

2.2填充构造

创建包含 n 个元素,每个元素初始化为指定值。

deque<string> dq2(5, "hello");
// dq2: ["hello", "hello", "hello", "hello", "hello"]

底层行为:

一次性分配足够块存储 n 个元素,避免多次扩容。

适用场景:

预先分配空间并初始化默认值(如游戏中的初始对象池)。

2.3范围构造(迭代器)

通过其他容器的迭代器范围初始化 deque

cpp 复制代码
vector<int> vec = { 1, 2, 3, 4, 5 };
deque<int> dq3(vec.begin(), vec.end());
// dq3: [1, 2, 3, 4, 5]

性能优势:

提前计算元素数量,优化块分配策略。

适用场景:

从数组、vector或文件数据快速构建deque

2.4拷贝构造

深拷贝另一个 deque 的所有元素。

deque<int> dq4 = { 1, 2, 3 };
deque<int> dq5(dq4); // dq5: [1, 2, 3]

注意:

对包含对象的deque,会调用元素的拷贝构造函数。

2.5移动构造(C++11+)

"窃取"另一个 deque 的资源(避免深拷贝)。

cpp 复制代码
deque<int> dq6 = { 10, 20, 30 };
deque<int> dq7(std::move(dq6));
// dq7: [10, 20, 30], dq6 变为空

适用场景:

函数返回deque时优化性能(如工厂模式创建队列)。

2.6初始化列表构造(C++11+)

直接用花括号初始化。

deque<int> dq8 = { 7, 8, 9 }; // dq8: [7, 8, 9]

底层机制:

先计算元素数量,分配连续块,再初始化值。

3. deque 常用成员函数

3.1增加元素:

push_back /emplace_back

功能:在尾部插入元素

区别:

push_back:拷贝或移动已有对象。

emplace_back:原地构造对象(避免临时对象拷贝)。

cpp 复制代码
deque<std::string> dq;
dq.push_back("hello");       // 拷贝构造
dq.emplace_back("world");    // 原地构造,效率更高

push_front / emplace_front

功能:在头部插入元素(vector 不具备的能力)。

dq.push_front("first"); // 头部插入
dq.emplace_front("second"); // 原地构造

性能:

头尾插入均为 O(1)(分摊时间复杂度)。

3.2删除元素:

pop_back / pop_front

功能:删除尾/头部元素(无返回值)。

dq.pop_back(); // 删除尾部
dq.pop_front(); // 删除头部

erase

功能:删除指定位置或范围的元素。

cpp 复制代码
deque<int>::iterator it = dq.begin() + 1;
dq.erase(it);                // 删除第2个元素
dq.erase(dq.begin(), it);    // 删除前两个元素

注意:

中间删除需移动元素,时间复杂度为O(n)

clear

功能:清空所有元素,释放内存块。

dq.clear();// size=0,但可能保留底层块(实现依赖)

3.3访问元素:

operator[] vs at()

方法 越界行为 性能
operator[] 未定义行为(可能崩溃) 更快
at() 抛出out_of_range 稍慢
cpp 复制代码
deque<int> dq = { 10, 20, 30 };
int a = dq[1];      // 20(不检查越界)
int b = dq.at(1);   // 20(越界时抛出异常)  

front() / back()

功能:直接访问首/尾元素(效率优于 dq[0])。

cpp 复制代码
int first = dq.front();  // 10
int last = dq.back();    // 30

迭代器访问

cpp 复制代码
for (deque<int>::iterator it = dq.begin(); it != dq.end(); ++it) {
    std::cout << *it << " ";  // 10 20 30
}

3.4容量与大小:

size() / empty()

cpp 复制代码
if (!dq.empty()) {
    cout << "元素数量: " << dq.size();  // 3
}

resize(n)

功能:调整元素数量,多删少补默认值。

dq.resize(5); // 扩容为5,新增元素默认初始化为0

dq.resize(2); // 截断后保留前两个元素

shrink_to_fit()

功能:请求释放未使用的内存块(非强制)。

dq.shrink_to_fit(); // 减少内存占用(实现可能忽略)

4. deque 的底层实现与性能分析

vector 不同,deque(双端队列)采用分段连续内存结构,不依赖单一大块内存,这使它在首尾两端插入/删除元素时拥有更优性能表现。

4.1底层结构:分段连续存储

核心组件

1.中控映射表(Map):

存储指向各个内存块的指针(类似指针数组)。

动态扩容时,中控表重新分配(通常加倍增长)。

2.固定大小的内存块(Chunk):

每个块存储若干元素(如 512 字节块存储 64 个 int)。

块之间内存地址不连续,但块内连续。

优势:

头尾插入无需移动所有元素,只需分配新块或复用空闲块。

随机访问通过计算块偏移实现。

4.2 动态扩容机制

扩容场景

1.头部插入时首个块已满:

分配新块插入头部,更新中控表。

2.尾部插入时末个块已满:

分配新块插入尾部,更新中控表。

与vector扩容对比

行为 deque vector
扩容触发 单个块满时 整体容量不足时
重新分配成本 仅扩展中控表,拷贝指针 拷贝所有元素到新内存
迭代器失效 局部失效(操作点附近) 全部失效

4.3时间复杂度总结

操作 deque vector 说明
push_front O(1) O(n) vector 需移动所有元素
push_back O(1) O(1) 两者均高效
随机访问 O(1) O(1) deque 有固定偏移计算开销
中间插入/删除 O(n) O(n) 均需移动元素

4.4关键问题

Q1:为什么 deque 不提供 capacity()

deque 的容量由中控表和块数量决定,无连续内存概念,capacity() 无意义。

Q2:deque 的内存碎片问题如何解决?

频繁小块分配可能导致碎片,可通过自定义分配器(Allocator)优化。

5. 算法库协作(STL 算法)

STL 中的算法(如 sort、find、count、transform 等)并不专门为某种容器设计,而是通过迭代器 操作容器元素。因此,deque 作为支持随机访问迭代器的容器,可以很好地与大多数 STL 算法配合使用

5.1排序算法(sort

基本用法

cpp 复制代码
#include<deque>
#include<algorithm>

using namespace std;

int mian(){
deque<int> dq = { 5, 3, 1, 4, 2 };
sort(dq.begin(), dq.end());  // 升序排序
// dq: [1, 2, 3, 4, 5]
}

输出内容:

性能陷阱

内存局部性影响:

deque 的分段存储导致排序时缓存命中率低于 vector,实测慢 10%-30%。

cpp 复制代码
// 测试:对 100,000 个元素排序 
sort(vec.begin(), vec.end());  // vector: ~15ms
sort(dq.begin(), dq.end());    // deque: ~20ms

优化建议

1.对全量排序需求,优先转存到 vector

cpp 复制代码
vector<int> tmp(dq.begin(), dq.end());
sort(tmp.begin(), tmp.end());
  1. 使用 std::stable_sort 保持相等元素顺序(deque 支持随机访问,兼容该算法)。

5.2查找算法

std::find 线性查找

cpp 复制代码
auto it = find(dq.begin(), dq.end(), 3);
if (it != dq.end()) {
    cout << "Found: " << *it;  // 输出: Found: 3
}

时间复杂度:O(n),与 vector 性能相当。

std::lower_bound 二分查找

前提:deque 必须已排序。

优势:O(log n) 时间复杂度。

cpp 复制代码
sort(dq.begin(), dq.end());
auto it = lower_bound(dq.begin(), dq.end(), 3);

std::count

统计容器中某个"特定值"出现的次数。

cout << "数值2的个数:" << count(dq.begin(), dq.end(), 2) << endl;

std::any_of

判断是否至少存在一个元素满足某个"条件"。

cpp 复制代码
 //是否存在偶数
 bool has_even = any_of(dq.begin(), dq.end(), [](int x) {return x % 2 == 0; });
 cout << "存在否:" << (has_even? "yes" : "no" )<< endl;

5.3函数式编程(for_each/transform

遍历元素

cpp 复制代码
for_each(dq.begin(), dq.end(), [](int x) {
     cout << x << " ";  // 打印所有元素
     });

元素转换

cpp 复制代码
 deque<int> dq = { 1,2,3,4,5 };
 deque<int> squared(dq.size());

transform(dq.begin(), dq.end(), squared.begin(),
     [](int x) { return x * x; });
 // squared: [1, 4, 9, 16, 25]

性能提示

deque 的遍历性能接近 vector(块内连续内存优势)。

避免在 transform 中频繁扩容,预分配结果容器大小。

6. 使用 deque 的注意事项

虽然 deque 是一个功能强大的双端队列容器,但在实际使用中也有一些易忽略的细节和潜在的坑点。

6.1迭代器失效规则

失效场景分类

操作类型 失效范围 示例
头尾插入/删除 可能使部分迭代器失效 push_front()、pop_back()
中间插入/删除 所有迭代器失效 insert()、erase()
排序/扩容 所有迭代器失效 std::sort()、resize()

危险代码示例

cpp 复制代码
deque<int> d = { 1, 2, 3 };
auto it = d.begin() + 1;
d.push_front(0);       // it 失效!
cout << *it;       // 未定义行为(崩溃)

安全实践

操作后重置迭代器:

it = dq.begin() + 2; // 重新计算位置

改用索引访问:

cpp 复制代码
size_t index = 1;
dq.push_front(0);
int val = dq[index];  // 安全

6.2插入/删除性能与位置有关

中间操作性能对比:

cpp 复制代码
deque<int> d(100000, 0);
auto start = chrono::high_resolution_clock::now();
d.insert(d.begin() + 50000, 42);  // 需移动 50,000 个元素
auto end = chrono::high_resolution_clock::now();
// 实测耗时:~200μs(vector 约 ~150μs)

结论

高频中间操作场景应选择listO(1)插入/删除)。

批量删除优化

使用 erase-remove 惯用法:

dq.erase(remove(dq.begin(), dq.end(), 0), dq.end());

6.3对象拷贝问题

深浅拷贝需求示例

cpp 复制代码
class FileHandle {

    FILE* file;

public:

    FileHandle(const char* filename) { file = fopen(filename, "r"); }
    ~FileHandle() { if (file) fclose(file); }
    
    // 必须实现深拷贝
    FileHandle(const FileHandle&) = delete;  // 禁止拷贝
    FileHandle& operator=(const FileHandle&) = delete;
};  

int main() {

    deque<FileHandle> dq;
    // dq.push_back(FileHandle("a.txt"));  // 错误!需要移动语义
    dq.emplace_back("a.txt");  // 正确:原地构造

}

解决方案

1.禁用拷贝:使用 =delete 显式禁止拷贝构造。

2.移动语义:实现移动构造函数和移动赋值运算符。

3.智能指针:存储 std::unique_ptr 而非直接对象。

6.4多线程安全

竞态条件示例

cpp 复制代码
deque<int> dq;
void unsafe_push() {
    for (int i = 0; i < 10000; ++i) {
        dq.push_back(i);  // 多线程下导致数据竞争
    }
}

线程安全方案

方案 适用场景 示例
互斥锁 低并发简单操作 std::mutex + lock_guard
读写锁 读多写少(C++17) std::shared_mutex
无锁队列 高性能场景 tbb::concurrent_queue

代码示例(互斥锁 ):

cpp 复制代码
mutex mtx;

void safe_push(int i) {
    lock_guard<std::mutex> lock(mtx);
    dq.push_back(i);
}

6.5 内存碎片问题

问题描述

deque 的分块存储可能导致内存碎片,尤其在频繁动态增长时。

解决方案

预分配块:

cpp 复制代码
deque<int> dq;
dq.resize(1000); //预分配固定数量元素

7. 实际应用场景

本章通过三个典型场景,展示 deque 如何解决实际问题。

场景 1:缓存系统的实现(LRU缓存淘汰策略)

需求分析

需要快速访问最近使用的数据。

当缓存满时,自动淘汰最久未使用的数据。

deque实现方案

cpp 复制代码
#include <iostream>
#include<deque>
#include<string>
#include<unordered_map> 

using namespace std; 

template <typename K, typename V>
class LRUCache {
private:

    deque<K> access_order;         // 保存键的访问顺序
    unordered_map<K, V> cache;     // 实际缓存内容,键值对存储
    size_t max_size;               // 缓存最大容量

    // 将访问过的 key 移动到队列前端(表示最近使用)
    void moveToFront(K key) {
        // 从 access_order 中删除 key 的旧位置(如果存在)
        // remove 并不真的删除元素,而是将所有 != key 的元素"前移",返回新逻辑结尾的迭代器
        // 然后用 deque 的 erase 将末尾被标记"移除"的元素真正删除
        access_order.erase(
           
           remove(access_order.begin(), access_order.end(), key),
            access_order.end());
        access_order.push_front(key);

    }

public:

    LRUCache(size_t size) : max_size(size) {}

    void put(K key, V value) {

        //如果缓存已满,淘汰 access_order 末尾的键(最久未访问)。
        if (cache.size() >= max_size) {
            K old_key = access_order.back();        // 淘汰最久未使用的
            cache.erase(old_key);
            access_order.pop_back();
        }

        //将新数据写入缓存,并将键放到 deque 前端。
        cache[key] = value;
        access_order.push_front(key);
    }

    V* get(K key) {

        auto it = cache.find(key);
        if (it == cache.end()) return nullptr;
        moveToFront(key);                           // 更新访问顺序
        return &(it->second);

    }

    //按照 access_order 顺序(最近→最久)打印当前缓存内容。
    void print() {
        for (const auto& key : access_order) {

            cout << "[" << key << ": " << cache[key] << "] ";
        }
        cout << endl;
    }

};

测试用例

cpp 复制代码
int main() {

    LRUCache<int, string> cache(3);
    cache.put(1, "A");
    cache.put(2, "B");
    cache.put(3, "C");
    cache.print();  // 输出: [3: C] [2: B] [1: A]
 
    cache.get(2);   // 访问键2
    cache.put(4, "D");  // 淘汰键1
    cache.print();  // 输出: [4: D] [2: B] [3: C]

}

输出内容:

场景 2:任务调度系统(动态优先级队列)

需求分析

支持从队列头部取出高优先级任务。

允许从尾部插入普通任务。

deque实现方案

cpp 复制代码
#include <iostream>
#include<deque>
#include<string>
#include<algorithm> 

using namespace std;
 
struct Task {
    string name;
    int priority; //优先级数值越小,优先级越高
  
    //重载运算符,用于排序
    bool operator<(const Task& other)const {

        return priority < other.priority;
    }
};

 

class PriorityTaskDueue {
private:

    deque<Task> tasks;
    bool is_sorted = false; //标记是否需要重新排序

public:

    //添加任务(头部添加高优先级任务,尾部添加普通任务)
    void addTask(const Task& task, bool is_high_priority = false) {
        if (is_high_priority) {
            tasks.push_front(task);
        } else {

            tasks.push_back(task);
        }
        is_sorted = false;
    }

    //处理最高优先级任务
    Task processNext() {

        if (tasks.empty()) {
            throw runtime_error("队列为空");
        }
 
        //按需要排序
        if (!is_sorted) {
            sort(tasks.begin(), tasks.end());
            is_sorted = true;
        }

        Task next_task = tasks.front();
        tasks.pop_front();
        return next_task;
    }

    //打印当前队列状态
    void printDeque() const {
        for (const auto& task : tasks)
            cout << "[" << task.priority << "]" << task.name << endl;
    }
};
 

int main()
{

    PriorityTaskDueue dq;
    dq.addTask({ "常规处理", 3 });
    dq.addTask({ "紧急处理",1 });
    dq.addTask({ "数据备份",2 });

    //处理任务
    cout << "当前队列:\n";
    dq.printDeque();
 
    auto task = dq.processNext();
    cout << "\n正在处理:" << task.name << endl;
    
    cout << "\n剩余队列:\n";
    dq.printDeque();  

}

输出内容:

性能优化策略

操作 时间复杂度 优化手段
addTask O(1) 头部/尾部直接插入
processNext O(n log n) 惰性排序(仅在实际需要时排序)
空间占用 O(n) 分段存储减少扩容开销

关键优化点:

  • 惰性排序:仅在 processNext() 时触发排序,避免频繁全队列排序。

  • 双端插入:高优先级任务直接 push_front,减少排序压力。

  • 动态任务队列管理(例如:任务队列的前后操作)

场景 3:滑动窗口最大值问题

需求分析

给定一个整数数组和窗口大小 k,返回每个窗口中的最大值。

要求时间复杂度优于暴力解法(O(nk))。

deque 实现方案(O(n)解法)

cpp 复制代码
#include <deque>
#include <vector>
#include<iostream> 

using namespace std;
 
vector<int> maxSlidingWindow(const vector<int>& nums, int k) {
    
    deque<int> dq;  // 存储索引,维护递减序列
    vector<int> result;
 
    for (int 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() {

    vector<int> nums = { 1, 3, -1, -3, 5, 3, 6, 7 };
    auto result = maxSlidingWindow(nums, 3);
    
    for (int num : result) {
        cout << num << " ";  // 输出: 3 3 5 5 6 7
    }
}

输出内容:

8. 总结

deque 核心优势

1.双端操作高效:push_front/pop_frontpush_back/pop_back 均为 O(1)

2.平衡的访问性能:随机访问 O(1)(稍慢于 vector),比 list10 倍以上。

3.动态分段存储:避免 vector 扩容时的全量拷贝,适合大规模数据。

何时选择 deque

高频头尾操作:消息队列、滑动窗口、撤销历史记录。

需要随机访问的双端队列:替代 list(如游戏中的事件处理)。

内存敏感场景:避免 vector 的扩容代价(如嵌入式设备)。

何时避免 deque

纯尾部操作 → 用 vector

高频中间插入/删除 → 用 list

严格内存连续 → 用 vector(如 C 接口交互)。

一句话决策

"头尾操作多,还要随机访,就用 deque 不会错;中间操作多,直接上 list 准没错。"

终极建议:

先测后选:用性能测试(如 Google Benchmark)验证实际场景。

保持简单:默认用 vector,遇到头插瓶颈再切 deque

相关推荐
左直拳3 小时前
c++中“&”符号代表引用还是取内存地址?
开发语言·c++·指针·引用·右值·取内存地址
十年编程老舅3 小时前
二本计算机,毕业=失业?
c++·程序员·编程·秋招·c++项目·春招·qt项目
虾球xz4 小时前
游戏引擎学习第260天:在性能分析器中实现钻取功能
网络·c++·学习·游戏引擎
Yusei_05234 小时前
C++复习类与对象基础
c++
拾忆-eleven6 小时前
C++算法(19):整数类型极值,从INT_MIN原理到跨平台开发实战
数据结构·c++·算法
Hxyle6 小时前
c++设计模式
开发语言·c++·设计模式
神仙别闹7 小时前
基于QT(C++)实现(图形界面)校园导览系统
数据库·c++·qt
明月看潮生7 小时前
青少年编程与数学 02-018 C++数据结构与算法 25课题、图像处理算法
c++·图像处理·算法·青少年编程·编程与数学
我是一只鱼02237 小时前
LeetCode算法题 (反转链表)Day17!!!C/C++
数据结构·c++·算法·leetcode·链表
菜鸟破茧计划8 小时前
C++ 算法学习之旅:从入门到精通的秘籍
c++·学习·算法