C++ stack和queue的使用及模拟实现
目录
stack
stack的使用
stack的OJ练习
1. 最小栈
2. 栈的压入,弹出序列
queue
queue的使用
容器适配器
本质:
栈和队列的模拟实现
栈的模拟实现
队列的模拟实现
总结:
stack
stack标准文档 : stack文档
翻译:
- stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
- stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
- 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
- 栈和队列都叫做适配器/配接器,不是直接实现的,而是封装其他容器,包装转换实现出来的。
stack的使用
主要函数介绍:
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
#include<iostream>
#include<stack>
#include<queue>
using namespace std;
void test_stack()
{stack<int> st;st.push(1);st.push(2);st.push(3);st.push(4);st.push(5);cout<<st.empty()<<endl;cout<<st.size()<<endl;while(!st.empty()){cout<<st.top()<<" ";st.pop();}cout<<endl;
}
int main()
{test_stack();return 0;
}
可以看出元素遵循后进先出的原则
stack的OJ练习
1. 最小栈
解题l代码:
class MinStack
{
public:void push(int val){_st.push(val);if(_minst.empty() || val<=_minst.top()){_minst.push(val);}}void pop(){if(_st.top()==_minst.top()){_minst.pop();}_st.pop();}int top(){return _st.top();}int getMin(){return _minst.top();}stack<int> _st;stack<int> _minst;
};
思路:
通过两个栈协同工作:
- 栈 _st :存储所有元素,负责普通的 push 、 pop 、 top 操作;
- 栈 _minst :仅存储当前栈中的“最小值序列”,栈顶始终是当前 _st 中的最小值。
操作逻辑:
- push 时:新元素先入 _st ;若 _minst 为空,或新元素小于等于 _minst 栈顶(保证最小值的“单调性”),则同时入 _minst 。
- pop 时: _st 正常出栈;若出栈元素等于 _minst 栈顶(说明这个最小值被弹出了),则 _minst 也出栈。
- getMin 时:直接返回 _minst 的栈顶(即当前全局最小值)。
2. 栈的压入,弹出序列
解题代码:
class Solution
{
public:bool IsPopOrder(vector<int> pushV,vector<int> popV){stack<int> st;size_t pushi = 0,popi = 0;while(pushi<pushV.size()){st.push(pushV[pushi++]);//栈中出的数据和出栈序列匹配上了while(!st.empty() && st.top() == popV[popi]){++popi;st.pop();}}return st.empty();//st为空,说明全都匹配了}
};
解题思路:
这道题的解题思路是模拟栈的压入和弹出过程,核心逻辑是通过一个辅助栈来还原“压入-弹出”的操作序列,判断目标弹出序列是否合法。
- 我们需要验证:给定“压入序列 pushV ”和“弹出序列 popV ”,是否存在一种栈操作的顺序,使得弹出序列完全匹配。
- 使用一个辅助栈 st ,并通过两个指针 pushi (指向压入序列的当前位置)和 popi (指向弹出序列的当前位置)来模拟过程:
- 压入阶段:按照 pushV 的顺序,将元素逐个压入辅助栈 st 。
- 弹出验证:每次压入后,检查栈顶元素是否与 popV[popi] 匹配。如果匹配,就弹出栈顶元素,并将 popi 后移(继续验证下一个弹出元素)。
- 当所有压入操作完成后,若辅助栈 st 为空,说明弹出序列完全匹配(返回 true );否则说明弹出序列不合法(返回 false )。
这种方法的时间复杂度是 O(n)( n 是序列长度,每个元素最多压入和弹出一次),空间复杂度是 O(n)(辅助栈的最大深度为 n ),是该问题的最优解法。
queue
queue文档 : queue标准文档
翻译:
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
- 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
主要函数介绍:
函数声明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
queue的使用
void test_queue()
{queue<int> q;q.push(1);q.push(2);q.push(3);q.push(4);q.push(5);cout << q.empty() << endl;cout << q.size() << endl;while(!q.empty()){cout<<q.front()<<" ";q.pop();}cout<<endl;
}
int main()
{test_queue();return 0;
}
可以看出遵循先进先出的原则
容器适配器
在 C++ 中,容器适配器(Container Adapter) 是一种“封装已有容器,通过限制接口来适配出特定数据结构行为”的设计。它本身不直接管理内存,而是依赖底层的“基础容器”来实现功能,核心是“适配接口,聚焦特定场景”。
本质:
容器适配器的核心逻辑是:选择一个“基础容器”(如 deque 、 vector 、 list ),然后只暴露该数据结构(栈、队列、优先队列)所需的接口,屏蔽基础容器的其他功能。
比如:
- 栈( stack )只需要“尾插、尾删、取尾元素”的接口,因此可以适配 deque (默认)、 vector 、 list ;
- 队列( queue )只需要“尾插入、头删除、取头尾元素”的接口,因此可以适配 deque (默认)、 list ;
“接口”是什么?
接口就是数据结构对外提供的“操作方式”。比如:
- 栈的接口是 push() (入栈)、 pop() (出栈)、 top() (取栈顶);
- 队列的接口是 push() (入队)、 pop() (出队)、 front() (取队头)、 back() (取队尾)。
这些接口是你操作这个数据结构的“入口”,你不需要关心它内部怎么实现,只要调用这些接口就能完成功能。
“暴露接口”的过程:适配器如何裁剪功能?
容器适配器的“暴露接口”,本质是“只开放底层容器中,适配目标数据结构所需的操作”,屏蔽其他无关操作。以栈( stack )为例,假设底层容器是 vector ( vector 本身支持随机访问、中间插入等很多操作):
- 栈只需要“尾插、尾删、取尾元素”的功能,所以适配器会只暴露 push() (调用 vector 的 push_back )、 pop() (调用 vector 的 pop_back )、 top() (调用 vector 的 back )这几个接口;
- 而 vector 原本的 insert() (中间插入)、 operator[] (随机访问)等功能,会被适配器“屏蔽”(因为栈不需要这些操作,暴露了反而会破坏栈“后进先出”的规则)。
举个代码例子(直观感受接口暴露)
#include <iostream>
#include <stack>
#include <vector>
using namespace std;int main() {// 栈的底层容器是 vector,但我们只能用栈的接口stack<int, vector<int>> st;st.push(1); // 入栈(暴露的接口,实际调用 vector::push_back)st.push(2);st.push(3);cout << st.top() << endl; // 取栈顶(暴露的接口,实际调用 vector::back)→ 输出3st.pop(); // 出栈(暴露的接口,实际调用 vector::pop_back)cout << st.top() << endl; // 输出2// 下面的操作是不被允许的!因为栈适配器没有暴露这些接口// st[0] = 10; // 试图随机访问vector的第一个元素 → 编译报错// st.insert(st.begin(), 0); // 试图在vector中间插入元素 → 编译报错
}
stack<int, vector<int>> st 这行代码的核心作用,就是显式声明栈( stack )这个适配器,使用 vector 作为它的底层容器。
- 如果不写第二个参数(即 stack<int> st ),栈会默认用 deque 作为底层容器;
- 而你手动指定了 vector<int> ,就相当于明确告诉编译器:“这个栈的底层存储,用 vector 来实现”。
可以看到,虽然底层是 vector ,但我们只能通过栈的接口( push 、 pop 、 top )来操作,无法直接使用 vector 的其他功能——这就是适配器“暴露所需接口,屏蔽无关功能”的过程。
下面我们再来会回看C++文档中对stack的说明:
这张图展示了 C++ 标准库中 std::stack 这个类模板的声明定义,它和你之前提到的 stack<int, vector<int>> st 是模板参数传递的关系,核心是解释 stack 适配器如何指定底层容器:
- 图中 template <class T, class Container = deque<T> > class stack; 表示:
- stack 是一个类模板,有两个模板参数: T (栈中存储元素的类型,比如 int )和 Container (底层容器类型,默认是 deque<T> )。
- 当你写 stack<int, vector<int>> st 时,就是显式地将第二个模板参数 Container 指定为 vector<int> ,从而改变了 stack 底层默认的 deque 容器,改用 vector 来存储元素。
- 如果只写 stack<int> st ,则会使用模板的默认参数 deque<int> 作为底层容器。
简单来说,这张图是 std::stack 的“模板定义说明书”,而 stack<int, vector<int>> st 是基于这个说明书的具体使用方式(手动指定底层容器为 vector )。
栈和队列的模拟实现
在了解了适配器之后 , 我们来进一步的学习stack和queue底层的模拟实现:
栈的模拟实现
栈满足后进先出的特性,在数据结构当中,我们可以使用顺序表和链表实现它,显然顺序表实现更优一些,因为顺序表进行尾插尾删的效率很高,而栈就是在一个方向上进行插入和删除。而无论是顺序表还是链表,都是可以实现栈这个数据结构的,所以我们可以封装容器,组合该容器中包含的成员函数。
#include<iostream>
#include<list>
#include<deque>
using namespace std;
//Stack
namespace Z
{ template<class T,class Container>class stack{private:Container _con;};
}
这段代码是自定义栈适配器的模板类框架,核心逻辑是通过封装一个底层容器(如 vector 、 deque 、 list 等),来实现栈的“后进先出”特性。
- 模板定义: template<class T, class Container> 表示这个栈是一个模板类,支持两种自定义类型: T (栈中存储的元素类型,比如 int 、 string )和 Container (底层容器类型,比如 vector<T> 、 deque<T> )。
- 底层容器封装: private: Container _con; 声明了一个底层容器对象 _con ,后续栈的所有操作(入栈、出栈、取栈顶等)都会通过调用 _con 的成员函数来实现(比如入栈调用 _con.push_back() ,出栈调用 _con.pop_back() 等)。
- 栈的所有操作都是通过底层容器 _con 来调用其对应的接口实现的。比如:
入栈 push 调用 _con.push_back(x) (本质是调用底层容器的尾插接口,比如 vector 的 push_back );出栈 pop 调用 _con.pop_back() (调用底层容器的尾删接口);取栈顶 top 调用 _con.back() (调用底层容器的取尾元素接口)。也就是说,栈适配器本身不直接管理元素的存储和操作,而是完全依赖底层容器 _con 提供的接口来实现栈的逻辑——这正是“容器适配器”的核心设计思想。简单来说,这段代码是栈适配器的“骨架”,通过模板和底层容器的组合,能灵活适配不同的元素类型和底层存储结构,实现栈的核心功能。
经过上面的介绍我们知道栈这个适配器也是一个模板,我们给栈这个适配器模板两个参数,一个是数据类型,一个是容器类型,成员是容器对象,通过它调用该容器的成员函数完成栈的功能
#include<iostream>
#include<list>
#include<deque>
using namespace std;
//Stack
namespace Z
{ class stack{//stack是一个Container适配(封装转换)出来的//template<class T,class Container = std::vector<T>>//可以给缺省类型template<class T,class Container = std::deque<T>>//可以给缺省类型//Container 尾认为是栈顶public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}const T& top(){return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};
}
这段代码是自定义栈适配器的完整实现,通过封装底层容器(默认是 std::deque<T> ,也可指定为 vector<T> 等)来实现栈的“后进先出(LIFO)”特性,核心逻辑如下:
1. 模板与底层容器设计
- 代码中 template<class T, class Container = std::deque<T>> 定义了一个模板类,支持两个参数:
- T :栈中存储的元素类型(如 int 、 string 等);
- Container :底层容器类型(默认使用 std::deque<T> ,也可手动指定为 std::vector<T> 、 std::list<T> 等,只要该容器支持 push_back 、 pop_back 、 back 、 size 、 empty 这些操作)。
2. 栈的核心接口实现
栈的所有操作都基于底层容器 _con 的成员函数封装:
- push(const T& x) :入栈,调用底层容器的 push_back ,将元素插入容器尾部(栈顶);
- pop() :出栈,调用底层容器的 pop_back ,删除容器尾部元素(栈顶);
- top() :取栈顶元素,返回底层容器的 back 引用;
- size() :返回底层容器的元素个数;
- empty() :判断栈是否为空,返回底层容器的 empty 结果。
下一步我们就可以这样显式实例化创建栈对象:
stack<int,std::vector<int>> st;
或者使用链表来构造栈数据结构:
stack<int,std::list<int>> st;
我们为了还可以这样创建栈对象,不显式实例化不传第二个参数:
stack<int> st;
为了可以这样我们在定义模板参数那里,给第二个参数缺省值:
template<class T,class Container = std::deque<T>>//可以给缺省类型
//template<class T,class Container = std::vector<T>>//可以给缺省类型
//template<class T,class Container = std::list<T>>//可以给缺省类型
这里给vector也可以,list也可以,deque也可以。deque是双端队列,下面会讲解deque容器。
我们来测试一下:
void test_stack1()
{stack<int, std::vector<int>> st;st.push(1);st.push(2);st.push(3);st.push(4);stack<int> st1;st1.push(10);st1.push(20);st1.push(30);st1.push(40);cout << "st:";while (!st.empty()){cout << st.top() << " ";st.pop();}cout << endl;cout << "st1:";while (!st1.empty()){cout << st1.top() << " ";st1.pop();}cout << endl;
}
在这个测试案例中:
- stack<int, std::vector<int>> st; 是**显式指定底层容器为 vector **的栈适配器;
- stack<int> st1; 由于没有指定第二个模板参数,会使用栈适配器的默认底层容器 deque (这与 C++ 标准库中 std::stack 的默认行为一致)。
可以看到我们实现的栈适配器是正确的
那还有一个问题 : 为什么编译器底层采取deque作为默认实现stack的适配器呢,而不是vector?
这是因为 deque 结合了 vector (连续内存、尾操作高效)和 list (分段内存、头操作高效)的优点,在栈的“尾插、尾删”场景下效率足够,同时还能应对一些边界情况(比如极端扩容时的性能表现)。而我们在模拟实现栈时选择 vector ,更多是出于教学直观性和逻辑简洁性的考虑(让学习者更容易理解栈是“顺序表尾部操作的封装”)。但在实际工程中,标准库的 std::stack 默认用 deque 是经过综合权衡的结果
队列的模拟实现
经过了栈适配器的模拟实现,实现队列适配器就很简单了,只需要注意Queue是队头出数据,队尾入数据,相当于是头删和尾插,因为vector容器没有实现头删,所以Queue不能封装vector:
//Queue
#include<iostream>
#include<list>
#include<vector>
#include<deque>
using namespace std;
namespace Z
{//Queue是一个Container适配(封装转换)出来的//template<class T,class Container = std::list<T>>//可以给缺省类型template<class T, class Container = std::deque<T>>//可以给缺省类型class queue{//Container 尾认为是队尾,头认为是队头,队头出数据,队尾入数据public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_front();}const T& front(){return _con.front();}const T& back(){return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};
}
这段代码是自定义队列适配器的完整实现,通过封装底层容器(默认是 std::deque<T> ,也可指定为 list<T> 等)来实现队列“先进先出(FIFO)”的特性,核心逻辑如下:
1. 模板与底层容器设计
- 代码中 template<class T, class Container = std::deque<T>> 定义了一个模板类,支持两个参数:
- T :队列中存储的元素类型(如 int 、 string 等);
- Container :底层容器类型(默认使用 std::deque<T> ,也可手动指定为 std::list<T> 等,只要该容器支持 push_back 、 pop_front 、 front 、 back 、 size 、 empty 这些操作)。
2. 队列的核心接口实现
队列的所有操作都基于底层容器 _con 的成员函数封装:
- push(const T& x) :入队,调用底层容器的 push_back ,将元素插入容器尾部(队尾);
- pop() :出队,调用底层容器的 pop_front ,删除容器头部元素(队头);
- front() :取队头元素,返回底层容器的 front 引用;
- back() :取队尾元素,返回底层容器的 back 引用;
- size() :返回底层容器的元素个数;
- empty() :判断队列是否为空,返回底层容器的 empty 结果。
这段代码模拟了 C++ 标准库 std::queue 的实现逻辑——通过“容器适配器”的思想,将底层容器的通用操作裁剪为队列的专用接口,从而在不重复实现底层存储的前提下,快速构建出队列的功能。
例如,若底层容器用 std::deque<T> ,则队列的“尾插入、头删除”就借助 deque 分段连续且头尾操作高效的特性;若用 std::list<T> ,则利用其链表结构在头尾操作时无需移动元素的优势。这种设计让队列的实现灵活且高效。
简单来说,这段代码是“自定义队列适配器”的教学级实现,清晰展示了“容器适配器如何封装底层容器以实现特定数据结构行为”的核心逻辑。
我们对队列进行测试:
void test_queue()
{//queue<int, std::vector<int>> q;//error,vector不支持头删头插,所以vector不能queue<int, std::list<int>> q;q.push(1);q.push(2);q.push(3);q.push(4);while (!q.empty()){cout << q.front() << " ";q.pop();}cout << endl;
}
int main()
{Z::test_queue();return 0;
}
- 测试自定义队列 Z::queue 的功能,验证其“先进先出”的特性。
- 选择 std::list<int> 作为队列的底层容器(也解释了为何不能用 vector ——因为 vector 不支持高效的头删操作,无法适配队列的出队逻辑)。
- 在这个测试案例中,选择 list 而非默认的 deque 主要是为了展示队列适配器对底层容器的“兼容性”,同时也能更清晰地体现不同容器的特性差异:
- 默认情况:自定义队列的默认底层容器是 deque (和标准库 std::queue 一致),它本身已经能很好地支持队列的“队尾入、队头出”操作。
- 选择 list 的意图:是说明只要容器支持 push_back 、 pop_front 、 front 、 back 等接口,就能作为队列的底层容器。 list 作为链表结构,在头尾操作时不需要移动元素,也能适配队列的逻辑,从而验证了适配器的灵活性。队列适配器的底层容器选择:需支持 push_back (队尾入队)、 pop_front (队头出队)等操作, list 和 deque 符合要求, vector 因头操作效率低而不适用。
自定义适配器的测试方法:通过模拟入队、出队流程,验证数据结构的行为是否符合预期。
总结:
本文介绍了C++中的容器适配器stack和queue的实现原理与使用方法。stack是后进先出(LIFO)的容器适配器,通过封装底层容器(vector/deque/list)的尾插尾删操作实现;queue是先进先出(FIFO)的容器适配器,通过封装底层容器(deque/list)的尾插头删操作实现。文章详细讲解了它们的模板设计、接口封装原理,并通过具体代码示例展示了如何自定义实现这两种适配器。同时分析了两者的底层容器选择策略,解释了为什么stack默认使用deque而queue不能使用vector的原因。最后通过OJ题目案例和测试代码验证了适配器的正确性。
感谢大家的观看!