C++ STL deque 双端队列:原理、实现与实战应用

发布时间:2026/7/22 5:03:53
C++ STL deque 双端队列:原理、实现与实战应用 1. deque是什么为什么你需要了解它如果你写过C用过vector也用过list那你可能会在某个时刻感到一丝纠结我需要一个能快速随机访问的容器但又在头部频繁插入删除vector的头部操作是O(n)的太慢了list倒是能在两端O(1)操作但随机访问又是O(n)的没法用下标直接定位。这个时候deque就该登场了。deque全称double-ended queue双端队列。这个名字很直白地告诉你它的核心能力两端进出。但它的内涵远不止于此。在C STL中deque被设计成一个“序列式容器”它承诺在两端进行插入和删除操作都拥有分摊常数时间复杂度同时支持随机访问通过operator[]和at()。这听起来有点像结合了vector和list的优点事实也的确如此虽然它在某些方面做了权衡。我第一次深入接触deque是在实现一个高性能的网络数据包缓冲区时。数据包从网络一端流入尾部插入从另一端被处理模块取出头部弹出同时我还需要能快速随机抽查某个位置的数据包进行校验。用vector头部弹出会导致大量数据移动用list随机抽查需要遍历。deque完美地解决了这个场景。理解deque不仅仅是多学一个容器更是理解STL设计者如何在连续存储与节点存储之间寻找精妙平衡点的绝佳案例。它适合所有已经熟悉vector和list并希望构建更复杂、性能要求更高的数据结构的C开发者。2. deque的整体设计与核心思路拆解deque的魔法在于其内部结构它并不是一块单纯的连续内存也不是简单的链表。标准并未规定其具体实现但主流实现如GCC的libstdc和Clang的libc都采用了一种类似“动态数组的数组”或“分段连续”的结构。2.1 核心数据结构中控器与缓冲区你可以把deque想象成一本活页夹。这本活页夹有一个目录中控器通常是一个指针数组目录的每一项记录着一个活页夹缓冲区一段固定大小的连续内存的起始地址。数据就存储在这些活页夹里。中控器 (Map) 这是一个指针数组每个指针指向一块缓冲区。中控器本身是动态分配的当活页夹数量增多目录不够用时可以重新分配一个更大的目录并将旧目录的条目复制过去这个过程开销相对较小因为只复制指针。缓冲区 (Buffer) 这是实际存储元素的内存块大小固定。在主流实现中缓冲区大小通常是这样计算的如果元素类型大小大于512字节则一个缓冲区只放一个元素否则缓冲区大小 512 / 元素大小。例如对于int4字节一个缓冲区可以存放512 / 4 128个int。这种设计带来了几个关键优势两端高效操作 在头部插入时如果第一个缓冲区还有空间就直接在前面放如果满了就在中控器头部新增一个缓冲区。尾部插入同理。这避免了vector那样整体搬移的巨大开销。随机访问 虽然数据在物理上是不完全连续的但通过计算可以快速定位。给定一个索引i我们可以通过i / 每个缓冲区的容量找到对应的中控器索引第几个活页夹再通过i % 每个缓冲区的容量找到在该缓冲区内的偏移量。这是一个O(1)的操作。迭代器设计复杂deque的迭代器比vector的普通指针复杂得多它需要记录当前元素指针、当前缓冲区起始指针、当前缓冲区末尾指针、以及指向中控器中当前位置的指针。这样在迭代器或--时才能判断是否需要跳到上一个或下一个缓冲区。2.2 与vector和list的权衡理解deque一定要放在与vector和list的对比中看特性std::vectorstd::dequestd::list内部结构单段连续内存分段连续内存中控器多缓冲区双向链表随机访问O(1) 极快O(1) 较快O(n) 慢头部插入/删除O(n) 慢除首元素外需移动分摊O(1) 快O(1) 快尾部插入/删除分摊O(1) 快分摊O(1) 快O(1) 快中间插入/删除O(n) 慢O(n) 慢O(1) 快已知位置迭代器类型随机访问迭代器随机访问迭代器双向迭代器内存局部性极好 数据完全连续较好 缓冲区内连续缓冲区间不连续差 节点分散内存开销低仅容量可能略大于大小中有中控器和可能未满的缓冲区开销高每个元素都有前后指针开销注意 这里deque的“分摊O(1)”需要理解。单次扩容新增缓冲区可能涉及中控器的重分配但平均到多次操作上时间复杂度是常数。它的性能表现非常稳定不像vector在扩容时会有一次明显的性能抖动。选择容器的简单指南需要极致随机访问速度且主要在尾部操作 - 选vector。需要频繁在任意位置插入删除且不需要随机访问 - 选list或forward_list。需要频繁在头部和尾部插入删除同时还需要不错的随机访问能力 - 这就是deque的主场。3. deque的核心接口解析与使用要点知道deque的原理后我们来看看怎么用它。它的接口和vector非常相似这降低了学习成本。3.1 基础构造与赋值#include deque #include iostream int main() { // 1. 默认构造 std::dequeint dq1; // 2. 使用n个val初始化 std::dequeint dq2(5, 100); // 包含5个100 // 3. 使用迭代器范围初始化 std::vectorint vec {1, 2, 3, 4, 5}; std::dequeint dq3(vec.begin(), vec.end()); // 拷贝vec的内容 // 4. 拷贝构造 std::dequeint dq4(dq3); // 5. 初始化列表 (C11) std::dequeint dq5 {10, 20, 30, 40}; // 赋值操作 dq1 dq5; // 拷贝赋值 dq2.assign(3, 50); // 重新赋值为3个50 dq3.assign(vec.begin(), vec.end()); // 用迭代器范围赋值 dq4.assign({1, 2, 3}); // 用初始化列表赋值 return 0; }3.2 关键特性操作两端操作这是deque的看家本领接口清晰明了std::dequeint dq; // 尾部操作 dq.push_back(1); // 尾部插入1 dq.emplace_back(2); // C11 尾部原位构造 效率可能比push_back高避免拷贝 int back_val dq.back(); // 获取尾部元素引用 dq.pop_back(); // 删除尾部元素 但不返回它 // 头部操作 (vector没有这些) dq.push_front(0); // 头部插入0 dq.emplace_front(-1); // 头部原位构造 int front_val dq.front(); // 获取头部元素引用 dq.pop_front(); // 删除头部元素 std::cout Front: dq.front() , Back: dq.back() std::endl;实操心得emplace_back和emplace_front是 C11 引入的“原位构造”方法。对于非平凡类型如自定义类push_back需要先构造一个临时对象再移动或拷贝到容器中而emplace_back直接传递构造参数给容器让容器在内存中直接构造对象省去了临时对象的创建和转移。在性能敏感的场景下应优先使用emplace_*系列函数。3.3 随机访问与容量查询deque支持像数组一样的随机访问这是它区别于list的关键。std::dequeint dq {10, 20, 30, 40, 50}; // 1. 使用 operator[] 不进行边界检查 访问更快 std::cout dq[2] std::endl; // 输出 30 dq[2] 300; // 可以修改 // 2. 使用 at() 进行边界检查 越界会抛出 std::out_of_range 异常 try { std::cout dq.at(10) std::endl; // 会抛出异常 } catch (const std::out_of_range e) { std::cerr Out of range error: e.what() std::endl; } // 3. 容量相关 (注意deque没有capacity()概念 因为它不是单段连续内存) std::cout Size: dq.size() std::endl; // 元素个数 std::cout Empty? dq.empty() std::endl; // 是否为空 // dq.max_size(); // 理论可容纳的最大元素数 通常很大 实际意义不大 // 4. 调整大小 dq.resize(8); // 将大小调整为8 新增的元素默认初始化(int为0) dq.resize(10, 999); // 将大小调整为10 新增的元素初始化为999 dq.resize(3); // 将大小调整为3 会丢弃尾部多余的元素重要区别vector有capacity()和reserve()来管理底层内存避免频繁扩容。但deque没有capacity()的概念因为它是由多个固定大小的缓冲区组成的。你无法为整个deque“预留”空间它的扩容是以缓冲区为单位的。这是使用deque时需要接受的一个设计差异。3.4 迭代器与遍历deque提供随机访问迭代器意味着它支持所有vector支持的迭代器操作包括迭代器加减数字。std::dequestd::string dq {Apple, Banana, Cherry, Date}; // 1. 常规迭代器遍历 std::cout Using iterators:\n; for (auto it dq.begin(); it ! dq.end(); it) { std::cout *it ; } std::cout std::endl; // 2. 基于范围的for循环 (C11) std::cout Range-based for loop:\n; for (const auto fruit : dq) { std::cout fruit ; } std::cout std::endl; // 3. 随机访问迭代器的威力可以直接加减 auto it dq.begin(); std::cout Third element: *(it 2) std::endl; // 输出 Cherry // 4. 反向迭代器 std::cout Reverse order:\n; for (auto rit dq.rbegin(); rit ! dq.rend(); rit) { std::cout *rit ; } std::cout std::endl;3.5 插入与删除除了两端deque也支持在任意位置插入删除但效率是O(n)因为可能需要移动多个缓冲区的元素。std::dequeint dq {1, 5, 9}; // 在指定位置前插入元素 auto it dq.begin() 1; // 指向5 dq.insert(it, 3); // deque变为 {1, 3, 5, 9} dq.insert(it, 2, 4); // 在it现在指向5前插入2个4 {1, 3, 4, 4, 5, 9} std::vectorint vec {7, 8}; it dq.end(); // 指向末尾 dq.insert(it, vec.begin(), vec.end()); // 在末尾插入vector范围 {1, 3, 4, 4, 5, 9, 7, 8} // C11 初始化列表插入 dq.insert(dq.begin(), {0, 0}); // 头部插入两个0 // 删除元素 dq.erase(dq.begin()); // 删除第一个元素 dq.erase(dq.begin() 2, dq.begin() 4); // 删除区间 [第三元素 第五元素) // 清空所有元素 dq.clear();注意事项insert和erase操作会使指向deque的所有迭代器、指针和引用失效。这是因为插入可能导致中控器重新分配虽然概率比vector小或者元素移动。这是一个常见的坑点。相比之下push_back和push_front通常只会使部分迭代器失效但最安全的做法是在插入/删除操作后不要继续使用旧的迭代器。4. 动手模拟实现一个简易deque理解了原理和接口我们尝试自己实现一个极度简化的MyDeque。这将彻底巩固你对deque内部机制的理解。我们的目标是实现一个能push_back、push_front、随机访问、并且能通过迭代器遍历的deque。4.1 定义缓冲区大小与核心数据结构首先我们定义一些常量和核心结构。我们的设计将采用经典的“中控器映射缓冲区”模型。// my_deque.h #pragma once #include cstddef #include iterator #include memory namespace my { // 假设缓冲区大小为 4 方便调试和观察。实际库实现会根据元素类型动态计算。 const size_t BUFFER_SIZE 4; template typename T class deque { private: // 中控器一个指针数组每个指针指向一个缓冲区T数组 T** map; // 中控器的容量能容纳多少个缓冲区指针 size_t map_capacity; // 中控器的中心位置或起始使用位置简化处理我们固定从中间开始使用 // 实际STL实现会更复杂有start和finish迭代器来记录首尾位置。 size_t map_center; // 迭代器类它是理解deque实现的关键 struct iterator { // 当前元素指针 T* cur; // 当前缓冲区起始指针 T* first; // 当前缓冲区结束指针最后一个元素的下一个位置 T* last; // 指向中控器中当前缓冲区指针的指针 T** node; // 迭代器分类 using iterator_category std::random_access_iterator_tag; using value_type T; using difference_type std::ptrdiff_t; using pointer T*; using reference T; // 构造函数 iterator(T* cur_, T** node_) : cur(cur_), node(node_) { first *node; last first BUFFER_SIZE; } // 解引用 reference operator*() const { return *cur; } pointer operator-() const { return cur; } // 前置 iterator operator() { cur; if (cur last) { // 如果到达当前缓冲区末尾 set_node(node 1); // 跳到中控器的下一个节点 cur first; // 当前指针指向新缓冲区的开始 } return *this; } // 后置 iterator operator(int) { iterator tmp *this; (*this); return tmp; } // 前置-- iterator operator--() { if (cur first) { // 如果到达当前缓冲区开头 set_node(node - 1); // 跳到中控器的上一个节点 cur last; // 当前指针指向新缓冲区的末尾 } --cur; return *this; } // 后置-- iterator operator--(int) { iterator tmp *this; --(*this); return tmp; } // 随机访问迭代器必须支持加减运算 iterator operator(difference_type n) { difference_type offset n (cur - first); if (offset 0 offset difference_type(BUFFER_SIZE)) { // 目标位置仍在当前缓冲区内 cur n; } else { // 需要跨缓冲区 difference_type node_offset offset 0 ? offset / difference_type(BUFFER_SIZE) : -((-offset - 1) / BUFFER_SIZE) - 1; set_node(node node_offset); cur first (offset - node_offset * BUFFER_SIZE); } return *this; } iterator operator(difference_type n) const { iterator tmp *this; return tmp n; } // 同理可以实现 - 和 - // 比较运算符 bool operator(const iterator other) const { return cur other.cur; } bool operator!(const iterator other) const { return !(*this other); } private: void set_node(T** new_node) { node new_node; first *new_node; last first BUFFER_SIZE; } }; // deque类的私有成员 iterator start; // 指向第一个元素 iterator finish; // 指向最后一个元素的下一个位置 // ... 其他辅助函数如 allocate_map, allocate_buffer 等 }; }这段代码定义了迭代器的核心。iterator内部保存了cur当前元素、first和last当前缓冲区边界以及node指向中控器对应槽位的指针。set_node函数用于在迭代器跨越缓冲区时更新这些指针。operator和operator--需要检查边界并可能跳转缓冲区这是deque迭代器与vector指针最根本的不同。4.2 实现内存管理与构造/析构接下来我们实现deque本身的内存管理、构造函数和析构函数。// 接在 my_deque.h 的 deque 类私有部分 private: // 分配指定数量的缓冲区指针空间中控器 void allocate_map(size_t n) { map_capacity std::max(n, size_t(8)); // 至少分配8个槽位 map new T*[map_capacity]; map_center map_capacity / 2; // 从中间开始使用方便两端扩展 } // 分配一个缓冲区 T* allocate_buffer() { return new T[BUFFER_SIZE]; } // 释放一个缓冲区 void deallocate_buffer(T* buffer) { delete[] buffer; } // 初始化中控器和迭代器 void initialize() { allocate_map(8); // 初始分配8个中控器槽位 // 在中间位置分配第一个缓冲区 map[map_center] allocate_buffer(); // 初始化start和finish迭代器都指向第一个缓冲区的起始位置此时容器为空 start iterator(map[map_center], map[map_center]); finish start; } // 销毁所有元素并释放内存 void destroy_all() { // 1. 调用所有已存在元素的析构函数 (简化版 假设是平凡类型 实际需用allocator) for (auto it start; it ! finish; it) { it-~T(); // 显式调用析构函数 } // 2. 释放所有缓冲区 for (size_t i 0; i map_capacity; i) { if (map[i] ! nullptr) { deallocate_buffer(map[i]); } } // 3. 释放中控器 delete[] map; } public: // 默认构造函数 deque() { initialize(); } // 析构函数 ~deque() { destroy_all(); } // 获取迭代器 iterator begin() { return start; } iterator end() { return finish; } // 基础功能 bool empty() const { return start finish; } size_t size() const { // 简化计算迭代器相减需要实现 operator- // 这里先返回一个粗略估计完整实现需要迭代器支持减法 // 临时方案遍历计数 size_t count 0; for (auto it start; it ! finish; it) count; return count; }这里我们实现了最简单的内存管理。allocate_map负责分配中控器数组allocate_buffer/deallocate_buffer负责单个缓冲区的生灭。initialize在构造函数中被调用它建立了一个初始状态一个中控器中间位置有一个空缓冲区start和finish都指向这个缓冲区的起始位置。destroy_all在析构时清理所有资源。4.3 实现push_back与push_front现在实现最核心的两端操作。这是deque性能优势的体现。// 在 my_deque.h 的 deque 类 public 部分继续添加 public: void push_back(const T value) { // 如果finish迭代器的cur不是当前缓冲区的最后一个位置last-1 if (finish.cur ! finish.last - 1) { // 当前缓冲区还有空间 *finish.cur value; // 在当前位置构造简化应使用placement new finish.cur; // finish迭代器向前移动一位 } else { // 当前缓冲区已满需要分配新缓冲区 push_back_aux(value); } } void push_front(const T value) { // 如果start迭代器的cur不是当前缓冲区的第一个位置first if (start.cur ! start.first) { --start.cur; // start迭代器向后移动一位 *start.cur value; // 在新位置构造 } else { // 当前缓冲区已满在头部需要分配新缓冲区 push_front_aux(value); } } T front() { if (empty()) { // 应该抛出异常这里简单处理 static T dummy; return dummy; } return *start; } T back() { if (empty()) { static T dummy; return dummy; } auto tmp finish; --tmp; return *tmp; } private: // 辅助函数当尾部缓冲区用完时调用 void push_back_aux(const T value) { // 检查中控器尾部是否还有空间存放新缓冲区的指针 // 这里简化处理假设总是有空间。实际需要判断并可能重分配中控器(map)。 size_t next_node_index (finish.node - map) 1; if (next_node_index map_capacity) { // 中控器需要扩容这是一个复杂操作此处省略。 // 实际应重新分配更大的map拷贝指针并更新start和finish的node指针。 throw std::bad_alloc(); // 简单抛出异常 } // 分配新缓冲区 map[next_node_index] allocate_buffer(); // 在新缓冲区的第一个位置构造元素 *(map[next_node_index]) value; // 简化构造 // 重新设置finish迭代器使其指向新缓冲区的第二个位置因为第一个位置已用 finish.set_node(map[next_node_index]); finish.cur finish.first 1; } void push_front_aux(const T value) { // 检查中控器头部是否还有空间 size_t prev_node_index (start.node - map) - 1; // 假设prev_node_index是有效的否则需要处理中控器前端扩容 // 分配新缓冲区 map[prev_node_index] allocate_buffer(); // 重新设置start迭代器使其指向新缓冲区的最后一个位置 start.set_node(map[prev_node_index]); start.cur start.last - 1; // 在当前位置构造元素 *start.cur value; }push_back的逻辑是如果尾部缓冲区还有空间就直接在finish.cur位置构造元素并移动finish.cur如果满了就调用push_back_aux分配新的缓冲区更新finish迭代器使其指向新缓冲区。push_front是镜像操作。这里省略了中控器(map)扩容这一复杂但关键的步骤在实际库实现中这会涉及重新分配指针数组、拷贝原有指针并更新所有迭代器内部的node指针是一个需要仔细处理的地方。4.4 实现随机访问operator[]最后我们实现随机访问这是体现deque“分段连续”计算逻辑的地方。public: T operator[](size_t n) { // 计算相对于起始位置的偏移 // 首先计算start迭代器到第一个缓冲区开头的偏移 size_t offset_from_start n (start.cur - start.first); // 计算目标缓冲区在中控器中的索引 size_t buffer_index offset_from_start / BUFFER_SIZE; // 计算在目标缓冲区内的偏移 size_t offset_in_buffer offset_from_start % BUFFER_SIZE; // 获取目标缓冲区的指针 T* target_buffer map[(start.node - map) buffer_index]; // 返回对应元素的引用 return target_buffer[offset_in_buffer]; } const T operator[](size_t n) const { // const版本同上 size_t offset_from_start n (start.cur - start.first); size_t buffer_index offset_from_start / BUFFER_SIZE; size_t offset_in_buffer offset_from_start % BUFFER_SIZE; T* target_buffer map[(start.node - map) buffer_index]; return target_buffer[offset_in_buffer]; }这个operator[]的实现清晰地展示了随机访问的O(1)过程通过简单的除法和取模运算定位到中控器的哪个缓冲区以及缓冲区内的哪个位置。注意计算起始偏移时(start.cur - start.first)这是因为start迭代器不一定指向缓冲区的起始位置first我们需要算出从逻辑容器起点到物理缓冲区起点的偏移量。4.5 一个简单的测试用例让我们写个简单的程序测试一下我们的MyDeque。// test_my_deque.cpp #include iostream #include my_deque.h int main() { my::dequeint dq; std::cout Testing push_back and push_front...\n; for (int i 0; i 5; i) { dq.push_back(i); // 尾部插入 0, 1, 2, 3, 4 } for (int i 5; i 10; i) { dq.push_front(i); // 头部插入 9, 8, 7, 6, 5 } // 此时deque内容从头到尾应为: 9 8 7 6 5 0 1 2 3 4 std::cout Deque size: dq.size() std::endl; std::cout Front: dq.front() , Back: dq.back() std::endl; std::cout Contents using iterator: ; for (auto it dq.begin(); it ! dq.end(); it) { std::cout *it ; } std::cout std::endl; std::cout \nTesting random access with operator[]:\n; for (size_t i 0; i dq.size(); i) { std::cout dq[ i ] dq[i] std::endl; } // 修改元素 dq[2] 100; dq[dq.size() - 1] 200; std::cout \nAfter modification: ; for (auto val : dq) { // 使用基于范围的for循环需要实现begin/end std::cout val ; } std::cout std::endl; return 0; }这个测试展示了基本的插入、遍历和随机访问功能。通过调试你可以观察start和finish迭代器内部cur和node的变化以及map指针数组的分配情况这对理解deque的运行机制非常有帮助。5. 使用deque的常见问题与实战技巧在实际项目中仅仅知道接口是不够的一些细节和陷阱决定了代码的健壮性和性能。5.1 迭代器失效问题详解这是使用STL容器最需要注意的问题之一deque也不例外。会使所有迭代器失效的操作insert在除首尾位置插入因为可能导致元素大范围移动或中控器重分配。erase在除首尾位置删除。任何可能导致中控器(map)重新分配的操作例如当两端同时快速增长中控器空间不足时。虽然我们的简易实现没做但标准库实现会做。会使部分迭代器失效的操作push_back和emplace_back通常不会使已有迭代器失效除非操作导致中控器重分配。push_front和emplace_front同上。pop_back和pop_front只会使指向被删除元素的迭代器失效其他迭代器通常安全。安全的做法在进行了任何插入或删除操作后如果后续逻辑还需要使用迭代器最安全的做法是重新获取迭代器例如it dq.begin()而不是继续使用旧的迭代器变量。实战示例std::dequeint dq {1, 2, 3, 4}; auto it dq.begin() 2; // it 指向 3 dq.push_front(0); // 在头部插入it **可能**仍然有效但不保证 // 保险起见如果需要使用指向元素3的迭代器应该重新计算 // it dq.begin() 3; // 重新获取 dq.insert(dq.begin() 1, 99); // 在中间插入it **肯定**失效不能再使用。5.2 性能考量与适用场景内存开销deque的内存开销比vector大因为它有中控器的开销和可能的缓冲区空间浪费缓冲区未满。如果元素数量非常少deque的相对开销会显得很大。遍历性能 由于数据不是完全连续存储遍历deque的CPU缓存友好性不如vector。在需要高强度顺序访问的场景下vector可能仍有优势。首选场景队列FIFO或双端队列Deque数据结构这是deque的经典用途例如任务队列、消息队列、滑动窗口等。需要频繁在序列两端进行插入删除同时偶尔需要随机访问。例如维护一个排序的列表需要在两端添加新元素又需要快速访问中间某个元素。作为stack和queue的默认底层容器。C STL中std::stack和std::queue的默认容器就是deque因为它提供了两端高效的操作。5.3 与vector和list的再对比与选择让我们通过一个具体例子来感受选择容器的差异。假设你要处理一个实时数据流需要保留最近N个数据样本。使用vectorstd::vectorint samples; // 当数据到来 samples.push_back(new_data); if (samples.size() N) { // 移除最旧的数据但vector头部删除是O(n) samples.erase(samples.begin()); // 性能瓶颈 } // 随机访问很快 int median samples[N/2];问题 当N很大时频繁的erase(samples.begin())会导致巨大的性能开销。使用liststd::listint samples; // 当数据到来 samples.push_back(new_data); if (samples.size() N) { samples.pop_front(); // O(1) 高效 } // 但如果你想计算中位数需要随机访问... auto it samples.begin(); std::advance(it, N/2); // O(n) 非常慢 int median *it;问题 随机访问是O(n)无法接受。使用deque最佳选择std::dequeint samples; // 当数据到来 samples.push_back(new_data); if (samples.size() N) { samples.pop_front(); // O(1) 高效 } // 随机访问也是O(1) int median samples[N/2]; // 高效deque在这里完美兼顾了两端的操作效率和随机访问需求。5.4 一个综合案例使用deque实现简单的LRU CacheLRU最近最少使用缓存是一种常见的缓存淘汰算法。我们可以用deque来记录访问顺序用unordered_map来快速查找。#include deque #include unordered_map #include iostream templatetypename K, typename V class SimpleLRUCache { private: size_t capacity_; // 存储键值对并保持访问顺序最近访问的放在尾部 std::dequestd::pairK, V cache_list_; // 快速通过key找到在deque中的迭代器 std::unordered_mapK, typename std::dequestd::pairK, V::iterator cache_map_; public: SimpleLRUCache(size_t capacity) : capacity_(capacity) {} V* get(const K key) { auto it cache_map_.find(key); if (it cache_map_.end()) { return nullptr; // 未找到 } // 找到将其移动到尾部标记为最近使用 auto list_it it-second; std::pairK, V kv *list_it; cache_list_.erase(list_it); // 从原位置删除 cache_list_.push_back(kv); // 插入到尾部 // 更新map中的迭代器指向新的位置 cache_map_[key] --cache_list_.end(); return (cache_list_.back().second); } void put(const K key, const V value) { auto it cache_map_.find(key); if (it ! cache_map_.end()) { // 键已存在更新值并移动到尾部 auto list_it it-second; cache_list_.erase(list_it); cache_list_.push_back({key, value}); cache_map_[key] --cache_list_.end(); } else { // 键不存在 if (cache_list_.size() capacity_) { // 缓存已满删除最久未使用的头部 auto lru_key cache_list_.front().first; cache_map_.erase(lru_key); cache_list_.pop_front(); } // 插入新键值对到尾部 cache_list_.push_back({key, value}); cache_map_[key] --cache_list_.end(); } } void print() const { std::cout LRU Cache (most recent - least recent): ; for (auto rit cache_list_.rbegin(); rit ! cache_list_.rend(); rit) { std::cout [ rit-first : rit-second ] ; } std::cout std::endl; } }; int main() { SimpleLRUCacheint, std::string cache(3); cache.put(1, Data1); cache.put(2, Data2); cache.put(3, Data3); cache.print(); // 输出: [3:Data3] [2:Data2] [1:Data1] auto val cache.get(2); // 访问key2 if (val) std::cout Got: *val std::endl; cache.print(); // 输出: [2:Data2] [3:Data3] [1:Data1] (2被移到最前) cache.put(4, Data4); // 插入新数据容量已满淘汰最旧的1 cache.print(); // 输出: [4:Data4] [2:Data2] [3:Data3] return 0; }在这个案例中deque的push_back标记最近使用、pop_front淘汰最久未使用和erase将元素从中间移到尾部操作都被用到了并且都是相对高效的操作。虽然erase是O(n)但在这个小规模缓存中是可以接受的。如果缓存规模极大可能需要更复杂的数据结构如链表哈希表但deque提供了一个简单清晰的实现起点。通过这个从原理到接口从模拟实现到实战案例的完整过程我希望你不仅学会了如何使用deque更理解了其内部设计精妙之处以及如何在合适的场景下选择它。记住没有最好的容器只有最合适的容器。deque正是在vector的连续性和list的节点灵活性之间找到了一个独特的、实用的平衡点。下次当你面临需要在序列两端频繁操作又不想放弃随机访问能力时别忘了你工具箱里的这个利器。