C++ deque 全面解析与实战指南

C++ deque 全面解析与实战指南

在C++标准模板库(STL)中,deque(double-ended queue,双端队列)是一种兼具vector和list部分特性的序列容器。它支持在队列的两端进行高效的插入和删除操作,同时也能像vector一样随机访问元素,是日常开发中处理动态数据的重要工具。本文将从deque的核心原理出发,详细讲解其常用接口,并结合实战案例演示如何在C++中使用deque,最后补充使用注意事项,帮助大家彻底掌握这一容器。

一、deque 核心原理剖析

要理解deque的特性,首先需要搞清楚它的底层实现。vector是基于连续的动态数组实现的,这使得它随机访问高效,但在头部插入/删除元素时需要移动大量数据,效率低下;list是基于双向链表实现的,头部和尾部插入/删除高效,但随机访问需要遍历链表,效率极低。而deque则采用了"分段连续存储"的设计,巧妙地平衡了两者的优缺点。

deque的底层结构可以理解为:一个指向多个连续内存块(缓冲区)的指针数组(也称为"中控器")。每个缓冲区存储一段连续的元素,中控器中的指针依次指向这些缓冲区。当需要在deque的两端插入元素时,如果当前缓冲区还有剩余空间,直接在对应端插入;如果缓冲区已满,则分配一个新的缓冲区,将其指针加入中控器,再进行插入操作。对于随机访问,deque会先通过中控器找到元素所在的缓冲区,再在缓冲区内部进行偏移访问,因此时间复杂度为O(1)。

核心优势总结:

  • 两端插入/删除效率高(O(1)时间复杂度);

  • 支持随机访问(operator[]、at()),效率接近vector;

  • 内存分配更灵活,避免了vector在扩容时可能出现的大量数据拷贝。

二、C++ deque 常用接口详解

deque的接口设计与vector类似,同时新增了针对双端操作的接口。使用deque前,需要包含头文件 <deque>,并使用std命名空间(或显式指定std::deque)。以下是最常用的接口分类讲解:

2.1 构造与析构

接口原型 功能说明 示例
deque(); 默认构造函数,创建空deque std::deque d;
deque(size_t n, const T& val = T()); 创建包含n个val的deque std::deque d(5, 10); // 5个10
deque(InputIterator first, InputIterator last); 迭代器构造,拷贝[first, last)区间元素 std::vector v{1,2,3}; std::deque d(v.begin(), v.end());
deque(const deque& other); 拷贝构造函数 std::deque d1{1,2}; std::deque d2(d1);
~deque(); 析构函数,释放所有资源 -

2.2 迭代器相关

deque支持双向迭代器,可通过迭代器遍历容器。需要注意的是,当deque进行插入/删除操作时,除了被操作元素所在的缓冲区外,其他缓冲区的迭代器不会失效(这一点与vector不同,vector扩容时所有迭代器都会失效)。

接口 功能说明
begin() 返回指向第一个元素的迭代器(非const)
end() 返回指向最后一个元素下一个位置的迭代器(非const)
rbegin() 返回指向最后一个元素的反向迭代器
rend() 返回指向第一个元素前一个位置的反向迭代器
cbegin()/cend() 返回const迭代器,不可修改元素
迭代器遍历示例:
cpp 复制代码
#include <deque>
#include <iostream>
using namespace std;

int main() {
    deque<int> d{1,2,3,4,5};
    // 正向遍历
    for (auto it = d.begin(); it != d.end(); ++it) {
        cout << *it << " "; // 输出:1 2 3 4 5
    }
    cout << endl;
    // 反向遍历
    for (auto it = d.rbegin(); it != d.rend(); ++it) {
        cout << *it << " "; // 输出:5 4 3 2 1
    }
    return 0;
}

2.3 容量相关

接口 功能说明
size() 返回当前元素个数
empty() 判断容器是否为空(空返回true)
resize(size_t n, const T& val = T()) 调整容器大小为n:n>当前size则补val;n<当前size则删除多余元素
capacity() 返回当前可容纳的元素个数(不同编译器实现可能不同,一般不常用)

2.4 元素访问

deque支持随机访问,访问方式与vector一致,但需要注意:deque没有reserve()接口,无法提前预留内存。

接口 功能说明 注意事项
operator[](size_t pos) 访问索引pos处的元素(不做越界检查) pos越界会导致未定义行为
at(size_t pos) 访问索引pos处的元素(做越界检查) pos越界会抛出out_of_range异常
front() 返回第一个元素的引用(非空容器) 容器为空时访问会导致未定义行为
back() 返回最后一个元素的引用(非空容器) 容器为空时访问会导致未定义行为

2.5 插入与删除(核心特性)

deque的核心优势在于两端的插入/删除操作高效,同时也支持在中间插入/删除(但效率较低,因为需要移动对应缓冲区的元素)。

接口 功能说明 时间复杂度
push_front(const T& val) 在头部插入元素val O(1)
push_back(const T& val) 在尾部插入元素val O(1)
pop_front() 删除头部元素(不返回值) O(1)
pop_back() 删除尾部元素(不返回值) O(1)
insert(iterator pos, const T& val) 在迭代器pos位置插入val O(n)(需移动元素)
erase(iterator pos) 删除迭代器pos位置的元素 O(n)(需移动元素)
clear() 清空容器,删除所有元素(size变为0) O(n)
插入/删除示例:
cpp 复制代码
#include <deque>
#include <iostream>
using namespace std;

int main() {
    deque<int> d;
    // 两端插入
    d.push_back(10);
    d.push_front(20);
    d.push_back(30);
    d.push_front(40);
    // 此时d:40 20 10 30
    for (auto x : d) cout << x << " "; // 输出:40 20 10 30
    cout << endl;
    
    // 两端删除
    d.pop_front();
    d.pop_back();
    // 此时d:20 10
    for (auto x : d) cout << x << " "; // 输出:20 10
    cout << endl;
    
    // 中间插入
    auto it = d.begin() + 1;
    d.insert(it, 15);
    // 此时d:20 15 10
    for (auto x : d) cout << x << " "; // 输出:20 15 10
    cout << endl;
    
    // 中间删除
    d.erase(it); // it仍指向原位置(此时对应15)
    // 此时d:20 10
    for (auto x : d) cout << x << " "; // 输出:20 10
    cout << endl;
    
    d.clear();
    cout << "size after clear: " << d.size() << endl; // 输出:0
    return 0;
}

三、deque 实战案例

下面通过两个经典场景,演示deque的实际应用:

3.1 场景1:实现一个简单的队列(先进先出)

队列的核心操作是"尾插首删",deque的push_back和pop_front接口恰好完美适配,效率远高于vector(vector.pop_front()为O(n))。

cpp 复制代码
#include <deque>
#include <iostream>
#include <string>
using namespace std;

template <typename T>
class MyQueue {
private:
    deque<T> dq;
public:
    // 入队
    void enqueue(const T& val) {
        dq.push_back(val);
    }
    // 出队
    void dequeue() {
        if (!dq.empty()) {
            dq.pop_front();
        } else {
            cout << "队列空,无法出队!" << endl;
        }
    }
    // 获取队首元素
    T front() const {
        if (!dq.empty()) {
            return dq.front();
        }
        throw runtime_error("队列空,无队首元素!");
    }
    // 判断队列是否为空
    bool empty() const {
        return dq.empty();
    }
    // 获取队列大小
    size_t size() const {
        return dq.size();
    }
};

int main() {
    MyQueue<string> q;
    q.enqueue("苹果");
    q.enqueue("香蕉");
    q.enqueue("橙子");
    
    cout << "队列大小:" << q.size() << endl; // 3
    cout << "队首元素:" << q.front() << endl; // 苹果
    
    q.dequeue();
    cout << "出队后队首:" << q.front() << endl; // 香蕉
    
    q.dequeue();
    q.dequeue();
    q.dequeue(); // 队列空,无法出队!
    
    return 0;
}

3.2 场景2:滑动窗口最大值(LeetCode 239)

这是deque的经典算法题,利用deque维护一个单调递减的队列,可在O(n)时间复杂度内解决问题。核心思路:

  • deque中存储的是元素的索引,而非元素本身,用于判断元素是否超出窗口范围;

  • 对于每个新元素,删除deque中所有小于当前元素的索引(保证deque单调递减);

  • 将当前元素索引加入deque;

  • 如果deque队首元素的索引超出窗口左边界,则删除队首;

  • 当窗口大小达到要求时,队首元素即为当前窗口的最大值。

cpp 复制代码
#include <deque>
#include <vector>
#include <iostream>
using namespace std;

vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    vector<int> res;
    deque<int> dq; // 存储索引,保持队列单调递减
    for (int i = 0; i < nums.size(); ++i) {
        // 1. 移除队列中小于当前元素的索引(保证单调递减)
        while (!dq.empty() && nums[i] > nums[dq.back()]) {
            dq.pop_back();
        }
        // 2. 加入当前元素索引
        dq.push_back(i);
        // 3. 移除窗口外的索引(窗口左边界为i - k + 1)
        while (!dq.empty() && dq.front() < i - k + 1) {
            dq.pop_front();
        }
        // 4. 当窗口大小达到k时,记录最大值(队首)
        if (i >= k - 1) {
            res.push_back(nums[dq.front()]);
        }
    }
    return res;
}

int main() {
    vector<int> nums = {1,3,-1,-3,5,3,6,7};
    int k = 3;
    vector<int> res = maxSlidingWindow(nums, k);
    cout << "滑动窗口最大值:";
    for (int x : res) {
        cout << x << " "; // 输出:3 3 5 5 6 7
    }
    return 0;
}

四、deque 使用注意事项

  1. 避免在中间频繁插入/删除:虽然deque支持中间插入/删除,但效率较低(O(n)),如果需要频繁在中间操作,建议使用list。

  2. 迭代器失效问题:deque插入/删除元素时,只有被操作元素所在的缓冲区的迭代器会失效,其他缓冲区的迭代器仍有效;而vector扩容时所有迭代器都会失效。

  3. 无reserve()接口:deque的内存分配是分段的,无法像vector那样提前预留连续内存,因此resize()和push_back()可能会触发多次小内存分配,但不会像vector那样出现大量数据拷贝。

  4. 与vector、list的选择

    • 需要随机访问 + 两端插入/删除高效 → 选deque;

    • 需要随机访问 + 主要在尾部插入/删除 → 选vector(效率更高);

    • 不需要随机访问 + 频繁在任意位置插入/删除 → 选list。

  5. 线程安全性:STL容器均不保证线程安全,多线程环境下操作deque需要手动加锁(如使用mutex)。

五、总结

deque是C++ STL中一种平衡了随机访问和双端操作效率的序列容器,其底层的分段连续存储设计使其兼具vector和list的部分优点。在实际开发中,deque常用于实现队列、滑动窗口等场景,尤其适合需要频繁在两端插入/删除且需要随机访问的需求。掌握deque的核心原理和常用接口,能帮助我们更灵活地处理动态数据,提升程序效率。

相关推荐
独自破碎E2 小时前
什么是RabbitMQ中的死信队列?
java·rabbitmq·java-rabbitmq
码界奇点2 小时前
基于Spring与Netty的分布式配置管理系统设计与实现
java·分布式·spring·毕业设计·源代码管理
计算机毕设指导62 小时前
基于微信小程序的咖啡店点餐系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·intellij-idea
Geoking.2 小时前
【设计模式】外观模式(Facade)详解
java·设计模式·外观模式
点云SLAM2 小时前
C++设计模式之单例模式(Singleton)以及相关面试问题
c++·设计模式·面试·c++11·单例模式(singleton)
saoys2 小时前
Opencv 学习笔记:图像膨胀 / 腐蚀(附滑块动态调节腐蚀核大小)
笔记·opencv·学习
ID_180079054732 小时前
除了Python,还有哪些语言可以解析淘宝商品详情API返回的JSON数据?
开发语言·python·json
闻道且行之2 小时前
NLP 部署实操:Langchain-Chatchat 配置文件深度修改与精细化调试
java·自然语言处理·langchain
h7ml2 小时前
企业微信回调模式解析:从XML到POJO的自定义JAXB编解码器设计
xml·java·企业微信