stack && queue 基本功能介绍、练习和模拟实现
- 前言
- 正式开始
- 基本函数功能
- 三道经典栈题目讲解
- 最小栈
- 栈的弹出压入顺序
- 逆波兰表达式求值
- 模拟实现
- stack
- queue
- deque
前言
本篇基本功能不会介绍太多,主要是说一下STL库中的接口,还是在这个网站上的:cplusplus
基本功能介绍完后会有几道题来帮助消化。
然后就是模拟实现了。
前面讲string、vector、list的时候都是将函数接口讲解和模拟实现分开来说的,但是栈和队列接口太少了,没必要分开说,再说我相信点进来的多少都是了解栈和队列的特性的。
或不多说,开始。
正式开始
先给两张图:
每句话前面圈红色的部分是适配器的意思。
啥时适配器呢?
现在怕是不好讲,等会讲模拟实现的时候你们就明白了。
这里再提一嘴,适配器是STL的六大组件(容器、适配器、迭代器、算法、函数对象、分配器)之一。
关于六大组件,这里给上一篇博客:C++ STL六大组件简介。如果看不懂没关系,多学就好。
基本函数功能
stack
queue
就这么几个函数接口,加起来还没一个vector的多。
有的同学可能要问为啥没迭代器啥的,因为栈和队列的特性是先进后出和后进先出,不需要我们去遍历,如果我们能够遍历栈和队列,就会出问题,其特性就会被改变。
那么就直接给例子了。
栈的:
这个例子已经把所有常用的接口都给了,就不细讲了,栈和队列重要的地方不是讲接口怎么用,重要的是有时做题要用到二者,重要的是解决问题的思路。
再给queue的例子:
也是不说那么多,front和back就是队头和队尾数据。
下面直接给题。
三道经典栈题目讲解
最小栈
链接:最小栈
题目如下:
题目解读:
本题是让我们实现一个栈,栈中存放的数据元素类型为int,这个栈要能够在O(1)时间复杂度下找出栈中的最小元素。
解题思路:
我们可以用两个栈来实现,一个普通栈来正常存放我们的数据,一个最小元素栈用来存放最小元素。
最小元素栈,当普通栈中push进一个小于等于历史最小元素的数据(假如为x)时候就要更新最小元素栈,将 x push到最小元素栈中。
代码实现:
class MinStack {
public:
// 这里的构造函数写不写无所谓,因为类中只有两个stack自定义类型,初始化时
//会自动调用stack的构造函数。这里就算构造函数是空的,也会在初始化列表处
//调用。把构造函数删除也是,会自动调用stack的构造函数。
MinStack() {
}
// push时若最小元素栈为空,就直接push进去,不为空,先比较,val小于等于栈顶就入栈
void push(int val) {
_normal_st.push(val);
if(_min_st.empty())
_min_st.push(val);
else if(val <= _min_st.top())
_min_st.push(val);
}
void pop() {
// 这里是否判断为空是无所谓的,因为逻辑上是不可能为空的。
if(!_min_st.empty() && _normal_st.top() == _min_st.top())
_min_st.pop();
_normal_st.pop();
}
int top() {
return _normal_st.top();
}
int getMin() {
return _min_st.top();
}
private:
stack<int> _normal_st;
stack<int> _min_st;
};
栈的弹出压入顺序
链接:栈的弹出压入顺序
题目:
题目解析:本题就是想要判断一下所给的入栈顺序是否能够实现对应的出栈顺序。
解题思路:模拟实现入栈过程。
每次pushV都直接入栈,入完栈后看栈顶元素与popV中的元素是否相等。
若不相等,则继续入栈;若相等,就pop栈,并继续比对popV的下一个元素与栈顶元素是否相等。
当push最后一个元素并比对完毕后,若栈不为空或popV中还有元素未比对,就返回false,若栈为空或popV中元素比对完毕,就返回true。
图解(能力有限,不会做动图,各位将就看看):
成立的:
[1,2,3,4,5],[4,5,3,2,1]
不成立的:
[1,2,3,4,5],[4,3,5,1,2]
bool IsPopOrder(vector<int>& pushV, vector<int>& popV)
{
int popI = 0;
stack<int> st;
for(auto puv : pushV)
{
st.push(puv);
while(!st.empty() && st.top() == popV[popI])
{
st.pop();
++popI;
}
}
return st.empty();
// return popI == popV.size();
//二者都可判断是否成立。
}
逆波兰表达式求值
链接:逆波兰表达式求值
题目:
题目解析:其实就是用后缀表达式求值。
我们平时1 + 1这样的表达式是中缀表达式,写成后缀就是1 1 +。
解题思路:定义一个栈,依次遍历字符串数组,遇到数字就入栈,遇到算数运算符就将栈顶两个元素做对应的运算,得到的结果继续入栈。
代码:
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
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();
}
};
中缀表达式转后缀表达式:
模拟实现
二者的模拟实现,只能说非常的简单,我们只需要复用vector/list的函数接口即可(其实不严谨,这个问题等会说)。
stack
代码如下:
和最上面一样的代码,测试:
queue
代码:
前面我说这里直接复用vector和list的函数接口不严谨,现在说一下这个问题。
这里栈和队列的模拟实现,和库中的是不一样的。
我们看一下库中的栈实现的代码:
其模板参数多了一个Sequence。
这个是干嘛的呢?
其实就是为了将stack搞成适配器。Sequence = deque<T>,这里缺省参数给的是deque,这个也是一个容器,等会再说。
库中stack的成员就一个Sequence c,这个c就相当于我们刚刚实现的vector _con。通过c来实现stack的基本接口,意思就是stack会复用一个默认的容器deque,通过这个deque来实现其基本函数接口,如果你想改变其底层实现stack的容器,就要自己传一个。
测试一下:
这才叫容器适配器,就是你传过去什么容器,就能用什么容器来实现其所有的功能。只要容器中有你所实现的stack函数接口中的所有功能,就是上面的c.接口。
我们还可以用list来实现:
这就是适配器。所以我们的代码也是要改一改的。
这样就和库中的一样了,记得用deque的时候要引对应头文件。
queue也改改:
但是queue不支持vector来进行实现,因为vector没有头删这个函数接口。
模拟实现就到这,然后说说deque这个容器。
deque
先看文档中的:
deque的函数接口非常的齐全。
支持头插头删、尾插尾删、任意位置的插入删除、[ ]重载等等。
那么其优势就出来了:
- 任意位置插入删除
- 支持随机访问
可以说是list和vector的结合体,但是其也有不好的地方。
这里就要说一下其底层是怎么实现的了。
deque实现,要开好多个小数组buffer,最前面小数组的buffer用来头插,最后小数组的buffer用来尾插,中间的数组就是存放中间数据了,只要一个小数组满了就再开一个小数组,小数组的大小都一样,都是存放n个数据。
还有一个中控数组,是一个指针数组,每个元素指向每一个小数组的首元素地址。
图画出来大概这样:
假如说其[ ]重载为:operator[](size_t i)
此处假设一个小数组buffer中有8个元素。
其[ ]的原理就是:
(i - 第一个buffer中元素个数) / 8得到该元素在第几个buffer中。
(i - 第一个buffer中元素个数) % 8得到其在这个buffer中是第几个元素。
然后说一下deque的迭代器。
先给张图:
这里是个略图,右下角是其迭代器,里面有四个指针,说一下各自的意思。
- cur指向当前所在的数据位置
- first和last表示当前所在buffer的开始和结束
- node指向中控数组中的指向当前所在缓冲区的结点指针。
我们看一下其是怎么deque怎么用迭代器遍历。
对于当前所在buffer,让cur一直++就可以了,等到cur走到last时再++,node就指向下一个buffer的结点指针,然后再让first和last更新为下buffer的头和尾,再让cur指向first就好了。
那么其缺陷就出来了
- operator[ ]计算略复杂,大量使用会导致性能下降。
- 中间插入删除效率不高。
- 底层迭代器会很复杂。
本来deque实现出来就是为了想成为vector和list的结合体的,但是实现出来了后也没有那么厉害,我们可以用一个排序的例子来证明一下:
十万个数:
可以看到vector是比deque快的。
deque就不讲那么多了,直接给结论:
- 头尾插入删除非常合适,相比vector和list而言。很适合做stack和queue的默认适配容器。
- 中间插入删除多用list。
- 随机访问多用vector。
就这么多,下一篇讲 优先级队列(也是容器适配器)。
到此结束。。。