STL Stack和Queue的模拟实现:从原理到手写代码
1. 引言STL容器适配器概念在C STL标准模板库中stack和queue并不是独立的数据结构它们被称为容器适配器Container Adapter。适配器模式将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。对于stack和queue而言它们并没有直接管理内存或存储元素而是通过封装另一个底层容器如deque、list、vector来实现其特定的接口。这种设计带来了高度的复用性我们只需要提供符合要求的底层容器stack和queue就能提供栈或队列的语义。为什么需要容器适配器代码复用直接利用现有容器如deque的强大功能无需重新实现内存管理、迭代器等复杂部分。接口约束栈和队列只允许在特定位置操作栈栈顶队列队尾进队头出。适配器通过封装底层容器的通用接口只对外暴露符合逻辑的特定接口从而避免了误操作。灵活性开发者可以根据性能需求指定不同的底层容器。2. Stack的深度剖析与模拟实现2.1 Stack的核心特性与接口栈Stack是一种后进先出LIFO, Last In First Out的数据结构。它只允许在容器的一端称为栈顶进行元素的添加压栈和移除出栈。标准接口基于SGI STL成员函数功能描述top()返回栈顶元素的引用不删除元素push(const T val)将元素压入栈顶pop()移除栈顶元素无返回值empty()判断栈是否为空size()返回栈中元素个数关键点pop()返回void而非元素值这是为了异常安全如果返回元素拷贝构造时可能抛异常导致元素已删除但未返回。top()提供可修改的引用。2.2 底层容器选择为什么是deque在SGI STL的实现中stack和queue的默认底层容器是deque双端队列。为什么选择deque而非vector或list对deque的考量push_back和pop_back高效deque在尾部插入和删除是O(1)的且不会像vector那样频繁发生内存重新分配和元素拷贝。随机访问能力虽然栈不需要随机访问但deque提供此功能并不妨碍使用。内存效率deque是分段连续空间相比list每个节点有前后指针更节省内存且缓存局部性更好。为什么不直接用vectorvector在尾部操作也是O(1)均摊但vector在push_back时如果容量不足会重新分配一块更大的内存将所有元素拷贝过去这在栈频繁操作时可能产生较大开销。而deque通过分段缓冲区避免了这种全局拷贝。不过如果需要严格的连续内存布局如与C API交互可以指定vector作为底层容器。为什么不直接用listlist在头尾操作也是O(1)但每个节点需要额外的指针开销通常8或16字节并且节点在内存中分散缓存命中率低。对于栈这种只在尾部操作的结构deque是更优选择。2.3 Stack的模拟实现模板类下面我们实现一个简化但功能完备的MyStack使用deque作为默认底层容器并允许用户指定其他容器。cpp#include deque #include stdexcept // for std::out_of_range namespace mystl { // 模板参数: T - 元素类型, Container - 底层容器类型默认dequeT template typename T, typename Container std::dequeT class MyStack { public: // 类型别名方便使用 using value_type typename Container::value_type; using size_type typename Container::size_type; using reference typename Container::reference; using const_reference typename Container::const_reference; // 构造函数默认使用底层容器的默认构造函数 MyStack() default; // 允许通过已有的容器构造可选 explicit MyStack(const Container cont) : c(cont) {} explicit MyStack(Container cont) : c(std::move(cont)) {} // 核心接口 // 判断是否为空 bool empty() const { return c.empty(); } // 返回元素个数 size_type size() const { return c.size(); } // 返回栈顶元素可修改 reference top() { // 栈顶是底层容器的末尾元素 if (empty()) { throw std::out_of_range(MyStack::top(): stack is empty); } return c.back(); } // 返回栈顶元素只读 const_reference top() const { if (empty()) { throw std::out_of_range(MyStack::top(): stack is empty); } return c.back(); } // 压栈在末尾添加元素 void push(const value_type value) { c.push_back(value); } // 压栈右值引用版本移动语义 void push(value_type value) { c.push_back(std::move(value)); } // 出栈移除末尾元素不返回值 void pop() { if (empty()) { throw std::out_of_range(MyStack::pop(): stack is empty); } c.pop_back(); } // 交换两个栈的内容 void swap(MyStack other) noexcept(noexcept(c.swap(other.c))) { c.swap(other.c); } // 为了支持范围for或迭代器通常栈不提供但我们可以提供底层容器的访问一般不推荐 // 这里为了调试或特殊需求可以暴露底层容器的引用谨慎使用 const Container get_container() const { return c; } private: Container c; // 底层容器 }; // 比较运算符通常使用底层容器的比较 template typename T, typename Container bool operator(const MyStackT, Container lhs, const MyStackT, Container rhs) { return lhs.get_container() rhs.get_container(); } template typename T, typename Container bool operator!(const MyStackT, Container lhs, const MyStackT, Container rhs) { return !(lhs rhs); } template typename T, typename Container bool operator(const MyStackT, Container lhs, const MyStackT, Container rhs) { return lhs.get_container() rhs.get_container(); } // ... 其他比较运算符类似实现 } // namespace mystl代码关键点解析模板参数Container的默认类型为std::dequeT体现了适配器模式的灵活性。底层成员Container c所有操作都转发给c。top()与pop()分离严格遵循STL设计pop()不返回值避免异常安全问题。异常安全在top()和pop()中对空栈进行检查并抛出异常模拟标准库行为标准库通常要求用户保证非空这里为了安全性加入检查。类型别名使用typename Container::xxx将底层容器的类型暴露出来使得MyStack的使用者可以获取元素类型等。2.4 性能分析与复杂度所有操作均委托给底层容器因此时间复杂度取决于底层容器push(): O(1) 均摊deque尾部插入pop(): O(1)top(): O(1)size(): O(1)通常容器维护大小empty(): O(1)空间复杂度O(N)N为元素个数。3. Queue的深度剖析与模拟实现3.1 Queue的核心特性与接口队列Queue是一种先进先出FIFO, First In First Out的数据结构。它允许在队尾back添加元素在队头front移除元素。标准接口成员函数功能描述front()返回队头元素的引用back()返回队尾元素的引用push(const T val)在队尾添加元素pop()移除队头元素无返回值empty()判断队列是否为空size()返回队列中元素个数3.2 底层容器的限制queue要求底层容器必须支持以下操作front()back()push_back()pop_front()size()empty()在STL容器中deque和list都满足这些要求而vector不满足不支持高效的pop_front()因此默认底层容器也是deque。3.3 Queue的模拟实现模板类cpp#include deque #include stdexcept namespace mystl { template typename T, typename Container std::dequeT class MyQueue { public: using value_type typename Container::value_type; using size_type typename Container::size_type; using reference typename Container::reference; using const_reference typename Container::const_reference; MyQueue() default; explicit MyQueue(const Container cont) : c(cont) {} explicit MyQueue(Container cont) : c(std::move(cont)) {} bool empty() const { return c.empty(); } size_type size() const { return c.size(); } // 返回队头元素 reference front() { if (empty()) throw std::out_of_range(MyQueue::front(): queue is empty); return c.front(); } const_reference front() const { if (empty()) throw std::out_of_range(MyQueue::front(): queue is empty); return c.front(); } // 返回队尾元素 reference back() { if (empty()) throw std::out_of_range(MyQueue::back(): queue is empty); return c.back(); } const_reference back() const { if (empty()) throw std::out_of_range(MyQueue::back(): queue is empty); return c.back(); } // 入队在尾部添加 void push(const value_type value) { c.push_back(value); } void push(value_type value) { c.push_back(std::move(value)); } // 出队移除头部元素 void pop() { if (empty()) throw std::out_of_range(MyQueue::pop(): queue is empty); c.pop_front(); } void swap(MyQueue other) noexcept(noexcept(c.swap(other.c))) { c.swap(other.c); } const Container get_container() const { return c; } private: Container c; }; // 比较运算符类似Stack实现 template typename T, typename Container bool operator(const MyQueueT, Container lhs, const MyQueueT, Container rhs) { return lhs.get_container() rhs.get_container(); } // ... } // namespace mystl3.4 性能分析与复杂度push(): O(1)pop(): O(1)deque和list头部删除均为O(1)front()/back(): O(1)size()/empty(): O(1)4. 底层容器deque的深度解析要真正理解stack和queue的默认行为必须深入剖析deque双端队列的实现原理。deque是最复杂的STL容器之一它提供了在两端进行O(1)插入删除的能力同时支持随机访问。4.1deque的设计哲学分段连续空间vector是连续的线性空间list是分散的节点。deque则采用了一种折中方案分段连续空间。它由一段一段的连续缓冲区buffer组成。这些缓冲区通过一个中央控制器map来管理。从用户角度看deque像一个连续的数组支持[]操作符但实际上元素可能分布在多个不连续的缓冲区中。这种设计带来了以下优点两端插入删除高效当在头部插入时如果第一个缓冲区前面有空间则直接使用否则在头部新增一个缓冲区。无需整体搬迁与vector不同deque在扩容时不会将所有元素拷贝到新内存只需在map中增加新的缓冲区即可。内存利用率较高比list节省指针开销比vector避免了预留大量内存。4.2 中央控制器的实现原理deque的核心是一个名为map的数组注意不是std::map它是一个指针数组每个指针指向一个缓冲区buffer。cpp// 伪代码示意 template typename T class deque { private: T** map; // 指向缓冲区的指针数组 size_t map_size; // map的大小 // 迭代器需要知道当前缓冲区、当前位置、以及map中的位置 // ... // 缓冲区大小通常是一个固定值如512字节 / sizeof(T) // 或者由编译器决定 };关键点缓冲区大小通常是一个固定值比如sizeof(T) 256 ? 4096 / sizeof(T) : 16保证缓冲区不会太大也不会太小。迭代器deque的迭代器不是普通的指针而是一个封装了当前节点、当前缓冲区、以及map中位置的复杂对象。4.3 迭代器的设计跨越缓冲区deque的迭代器需要实现以下功能解引用返回当前缓冲区中当前位置的元素。自增/自减当到达当前缓冲区的末尾或开头时需要跳转到下一个或上一个缓冲区。迭代器结构简化cpptemplate typename T struct _Deque_iterator { T* cur; // 当前缓冲区中的当前元素 T* first; // 当前缓冲区的起始位置 T* last; // 当前缓冲区的结束位置最后一个元素的下一个位置 T** node; // 指向map中当前缓冲区指针的指针 // 自增操作 void operator() { cur; if (cur last) { // 当前缓冲区结束 set_node(node 1); // 切换到下一个缓冲区 cur first; } } void set_node(T** new_node) { node new_node; first *new_node; last first buffer_size(); } // 其他操作... };这种复杂的迭代器设计使得deque的随机访问operator[]比vector慢一些因为它需要计算元素位于哪个缓冲区以及偏移量。4.4deque与vector、list的对比特性dequevectorlist内存布局分段连续连续非连续节点头部插入/删除O(1)O(N)O(1)尾部插入/删除O(1)O(1) 均摊O(1)随机访问O(1)但常数较大O(1)O(N)迭代器类型随机访问迭代器随机访问迭代器双向迭代器内存开销低除缓冲区外有map开销低无额外指针高每个节点两个指针重新分配添加缓冲区不移动元素整体搬迁可能移动元素不重新分配只分配节点缓存局部性较好连续缓冲区最好差结论对于stack只需尾部操作和queue需头尾操作deque在性能、内存和灵活性上达到了最佳平衡。5. 扩展与优化5.1 指定底层容器在STL和我们的实现中都可以指定底层容器。例如cpp// 使用vector作为栈的底层容器注意vector不支持pop_front所以不能用于queue mystl::MyStackint, std::vectorint vec_stack; // 使用list作为队列的底层容器 mystl::MyQueueint, std::listint list_queue;注意如果指定的容器不满足接口要求编译时会报错例如vector用于queue时缺少pop_front。5.2 线程安全问题探讨STL容器包括stack和queue的成员函数不是线程安全的。多线程只读多个线程同时调用const成员函数如top()、empty()、size()通常是安全的前提是没有线程在修改。多线程读写如果有任何线程在修改容器如push()、pop()则必须使用外部同步机制如互斥锁。常见实践cppstd::stackint s; std::mutex mtx; void thread_safe_push(int val) { std::lock_guardstd::mutex lock(mtx); s.push(val); }C标准库提供了std::stack和std::queue但没有提供线程安全版本。5.3 实际应用场景stack的应用函数调用栈递归实现表达式求值中缀转后缀括号匹配深度优先搜索DFSqueue的应用任务调度队列广度优先搜索BFS消息队列生产者-消费者模式缓冲池6. 总结本文深入探讨了STL中stack和queue的设计与实现主要内容包括容器适配器概念理解stack和queue如何通过封装底层容器来提供特定的接口。模拟实现手写了MyStack和MyQueue展示了模板编程、异常安全、接口设计等要点。底层容器解析详细剖析了deque的分段连续存储结构、中央控制器map、迭代器实现以及它为何成为默认底层容器。性能对比对比了deque与vector、list在栈和队列场景下的优劣。扩展知识讨论了指定底层容器、线程安全、实际应用等话题。