👻内容专栏: C/C++编程
🐨本文概括:stack与queue的介绍与使用、模拟实现。
🐼本文作者: 阿四啊
🐸发布时间:2023.10.17
一、stack的介绍与使用
1.1 stack的介绍
以下是stack
的文档介绍
stack
是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。stack
是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。stack
的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
- 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
1.2 stack的使用
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
void test_stack()
{
stack<int> st;
//元素入栈
st.push(1);
st.push(2);
st.push(3);
st.push(4);
while (!st.empty())
{
//打印栈顶元素
cout << st.top() << " ";
//出栈
st.pop();
}
cout << endl;
}
下面刷一道leetcode上有关用栈来解决的题目:逆波兰表达式
基本思路:
1.首先匹配到数字元素就入栈;
2.如果匹配到符号,就出栈顶元素进行计算,第一次出栈顶元素表示右操作符,第二次出栈顶元素表示左操作符,运算结果入栈。
3.最后栈顶的唯一元素即为运算的结果,返回栈顶元素即可。
代码实现:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
//myStack::stack<int,vector<int>> st;
//myStack::stack<int,list<int>> st;
myStack::stack<int> st;
for (auto& str : tokens)
{
if (str == "+" ||
str == "-" ||
str == "*" ||
str == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch (str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
}
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
1.3 stack的模拟实现
我们细心观察文档发现,stack是一个container adaptor
,意为容器适配器,它使用的底层容器来存储元素,并提供了一组特定的操作来实现栈的功能。
容器适配器是一种设计模式,它允许使用不同的底层容器来实现相同的功能。在Stack中,可以使用不同类型的容器作为底层实现,例如vector、deque或list。这样可以根据具体的需求选择最适合的底层容器。
stack.h文件中:
namespace myStack
{
//Container不管是什么样的底层容器,都可以适配出想要的栈
//库中的Container缺省参数为deque双端队列
//template<class T,class Container = deque<T>>
template<class T, class Container>
class stack
{
public:
stack()
{}
void pop()
{
_con.pop_back();
}
void push(const T& x)
{
_con.push_back(x);
}
T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
在test.cpp文件中进行测试(以上面一题的代码为例):
//测试stack
#include "stack.h"
class Solution {
public:
int evalRPN(vector<string>& tokens) {
//myStack::stack<int,vector<int>> st;
myStack::stack<int,list<int>> st;
for (auto& str : tokens)
{
if (str == "+" ||
str == "-" ||
str == "*" ||
str == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch (str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
}
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
int main()
{
vector<string> str = { "2","1","+","3","*" };
Solution s;
cout << s.evalRPN(str) << endl;
return 0;
}
📌我们用vector和list去适配都能达到想要的结果。在使用库中的list时,我们定义一个list时,并没有实例化给出两个模板参数,这是因为库中有个默认的模板参数Container = deque<T>
,至于deque是什么,其名为双端队列,双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象。deque可以说同时具备vector(下标的随机访问)与list(大量中间位置的插入删除)的优点,但是呢deque就好像一个六边形战士,好像什么都会,其实都不厉害,造成了“会而不精”的假象。
后面篇章我们会详细讲解deque.先暂做了解即可。
二、queue的介绍与使用
2.1 queue的介绍
以下为queue
的文档介绍
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty
:检测队列是否为空size
:返回队列中有效元素的个数front
:返回队头元素的引用back
:返回队尾元素的引用push_back
:在队列尾部入队列pop_front
:在队列头部出队列
- 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque
2.2 queue的使用
函数说明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
void test_queue()
{
queue<int> q;
//在队列尾部入队列
q.push(1);
q.push(2);
q.push(3);
q.push(4);
while (!q.empty())
{
//返回队头元素
cout << q.front() << endl;
//在队列头部出队列
q.pop();
}
cout << endl;
}
2.3 queue的模拟实现
因为queue的接口中存在频繁的头部插入与删除操作,因此使用vector来封装效率太低(STL库中也没有支持对于头部插入删除的操作),故可以借助list来模拟实现queue。
具体如下:
queue.h文件中:
namespace myQueue
{
//template<class T, class Container = deque<T>>
template<class T, class Container>
class queue
{
public:
queue(){}
T& front()
{
return _con.front();
}
T& back()
{
return _con.back();
}
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
对queue的测试:
#include "queue.h"
//queue的测试
int main()
{
//使用vetcor会报错,显示pop_front不是其成员,
//myQueue::queue<int, vector<int>> q;
//queue<int, vector<int>> q;
//vector容器不能适配队列,队列标准是先进先出,而vector不支持头插头删
myQueue::queue<int, list<int>> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
while (!q.empty())
{
cout << q.front() << " ";
q.pop();
}
cout << endl;
return 0;
}
同样的,我们在使用库中的queue时,平时也只用传入一个参数进行实例化对象,这是因为库中使用了一个缺省的模板参数Container
,底层默认容器为deque.
三、为什么选择deque作为stack和queue的底层默认容器
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
2.在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。