文章目录
一、适配器模式
设计模式
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可用性,可维护性,可读性,稳健性以及安全性的解决方案
迭代器模式
迭代器模式(Iterator Pattern) :提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式,使得在不暴露底层实现细节的情况下,封装后让我们使用相同的方式来访问不同的容器
适配器模式
Adapter(适配器模式)属于结构型模式,结构性模式关注的是如何组合类与对象,将一个类的接口转换成客户希望的另外一个接口,以获得更大的结构,即将已有的东西转换成我们想要的东西
二、stack
1.stack的介绍
我们和之前一样,参照 cplusplus 网站进行学习:stack文档介绍
和vector、list不同,栈是一种受特殊限制的线性表,需要保证FILO(先进后出)的特性,stack不提供迭代器,所以stack不是迭代器模式,而是一种容器适配器:
stack和queue都采用deque容器作为默认的适配器,我们stack,queue也可以使用vector,list作为适配器.
【总结】
1.stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作
2.stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出
3.stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
push_back:尾部插入元素操作
4.标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque
2.stack的使用
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
void stack_test()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
cout << st.size() << endl;
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
cout << endl;
}
3.stack的模拟实现
我们在了解适配器模式之后,我们可以将适配器作为类的第二个模板参数,然后通过传递不同的适配容器来进行适配,我们可以使用vector、list作为stack的适配容器,但是出于随机访问和CPU高速缓存命中率的考虑 ,我们最终选择vector作为stack的适配容器,我们可以将vector作为缺省参数,以后定义时我们就不需要显示传递vector了
// stack.h
template<class T,class Container=vector<T>>
class stack
{
// ...
};
// test.cpp
void stack_test()
{
// 默认使用vector作为适配容器
stack<int> st1;
// 使用list作为适配容器
stack<int, list<int>> st2;
}
stack.h
#pragma once
#include <vector>
#include <list>
#include <queue>
namespace hdp
{
//template<class T, class Container = vector<T>>
template<class T, class Container = deque<T>>
class stack
{
public:
// 我们不需要显示写构造和析构函数,编译器对于自定义类型会去调用其自身的默认构造
// 入栈
void push(const T& x)
{
_con.push_back(x);
}
// 出栈
void pop()
{
_con.pop_back();
}
// 获取栈顶元素
const T& top()
{
return _con.back();
}
// 判空
bool empty()
{
return _con.empty();
}
// 获取元素个数
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
test.cpp
#include <iostream>
#include <queue>
#include "stack.h"
using namespace std;
// 测试栈
void stack_test()
{
hdp::stack<int, deque<int>> st;
// 入栈
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
// 获取元素个数
cout << st.size() << endl;
while (!st.empty())
{
// 获取栈顶元素
cout << st.top() << " ";
// 出栈
st.pop();
}
cout << endl;
}
int main()
{
stack_test();
return 0;
}
4.stack的相关OJ题目
题目描述
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
- MinStack() 初始化堆栈对象。
- void push(int val) 将元素val推入堆栈。
- void pop() 删除堆栈顶部的元素。
- int top() 获取堆栈顶部的元素。
- int getMin() 获取堆栈中的最小元素。
示例:
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]输出:
[null,null,null,null,-3,null,0,-2]解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
思路:
我们可以使用一个minST来保存最小值,当插入的元素小于minST栈顶元素的时候就将该元素插入到minST中,如果该元素大于或者等于minST栈顶元素的时候就向minST中插入一个与栈顶元素大小相同的值,此时最小值就是minST栈顶的元素,st pop的时候minST也同时进程pop
st:[8,9,9,7,2,9]
minST:[8,8,8,7,2,2]
// pop
st:[8,9,9,7,2]
minST:[8,8,8,7,2]
min = 2
我们对上面的思路可以进行优化,因为它的空间复杂度为O(N),需要一个同等大小的栈来保存数据,优化思路为:我们不需要每次都向minST插入元素,只有当插入的元素小于或者等于minST栈顶的 元素的时候才插入,pop数据的时候只有当pop的值等于minST栈顶元素的时候才同时进行pop
st:[8,9,9,7,2,9]
minST:[8,7,2]
// pop
st:[8,9,9,7,2]
minST:[8,7,2]
min = 2
// pop
st:[8,9,9,7]
minST:[8,7]
min = 7
我们需要注意的是,当插入的元素与minST栈顶元素相等的时候也要插入,这是为了避免后续插入的元素中有等于当前minST栈顶的元素,这样pop元素之后会导致minST栈顶元素可能就不再是最小值了
代码实现:
class MinStack {
public:
// 可以为空,因为初始化类别会进行初始化
MinStack()
{}
void push(int val)
{
// minST为空或者插入的元素小于或者等于minST栈顶元素的时候插入
if(minst.empty() || val<=minst.top())
minst.push(val);
st.push(val);
}
void pop()
{
// 当pop的数据和minST栈顶元素相同的时候才pop
if(minst.top()==st.top())
minst.pop();
st.pop();
}
int top()
{
return st.top();
}
int getMin()
{
return minst.top();
}
private:
stack<int> st;
stack<int> minst;
};
题目描述
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
1.0<=pushV.length == popV.length <=1000
2.-1000<=pushV[i]<=1000
3.pushV 的所有数字均不相同
示例
输入:[1,2,3,4,5],[4,5,3,2,1]
返回值:true
说明:
可以通过push(1)=>push(2)=>push(3)=>push(4)=>pop()=>push(5)=>pop()=>pop()=>pop()=>pop()
这样的顺序得到[4,5,3,2,1]这个序列,返回true
思路:
这道题我们只需要模拟出栈的顺序即可,将pushV中的元素入栈,入栈的元素和popV的元素进行比较,如果相同,则说明当前元素此时出栈,所以栈顶的元素就出栈,如果不相等pushV就继续入栈,当全部入栈之后,如果pushV中入栈的元素全部被pop(pushV为空),就说明出栈顺序是正确的
代码实现:
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV)
{
//入栈和出栈的元素个数必须相同
if(pushV.size() != popV.size())
return false;
stack<int> st;
size_t popi=0;
for(auto e:pushV)
{
st.push(e);
// 跟出栈序列比较,相等就出栈 因为可能多个元素持续出栈,所以是while不是if
while(!st.empty()&&st.top()==popV[i])
{
st.pop();
++popi;
}
}
return st.empty();
}
};
题目描述
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
- 每个操作数(运算对象)都可以是一个整数或者另一个表达式
- 两个整数之间的除法总是 向零截断
- 表达式中不含除零运算
- 入是一个根据逆波兰表示法表示的算术表达式
- 答案及所有中间计算结果可以用 32 位 整数表示
示例
输入:tokens = [“2”,“1”,“+”,“3”,“*”]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
思路:
对于这道题,我们首先需要明白计算机对逆波兰表达式(后缀表达式)的计算方法:用栈来存储数据,从左到右遍历表达式,遇到操作数就入栈,遇到操作符就取出栈的两个数据进行运算,注意第一次栈顶的元素为右操作数,第二次取的栈顶的数据为左操作数,然后将运算的结果入栈,然后继续往后进行遍历,直到将表达式遍历完毕
代码实现:
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();
if(str=="+")
st.push(left+right);
if(str=="-")
st.push(left-right);
if(str=="*")
st.push(left*right);
if(str=="/")
st.push(left/right);
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
知识拓展–中缀表达式转后缀表达式
中缀表达式计算我们生活中所见到的表达式,但是计算机更容易计算后缀表达式,所以计算机在计算的时候需要先将中缀表达式转为后缀表达式,再根据后缀表达式来进行求值
中缀表达式转后缀表达式规则如下:
1.遇到操作数直接输出
2.使用栈来存储操作符,遇到操作符如果栈为空就直接入栈,如果栈不为空,则与栈顶的操作符进行比较,如果当前操作符优先级比栈顶的优先级高,则入栈,如果低于或者等于,则栈顶的操作符出栈,当前操作符入栈
举例说明:
中缀表达式:1+2*3/4-5
// 遇到操作数直接输出
st:[] 后缀表达式 1
// 遇到操作符且栈为空,将操作符入栈
st:[+] 后缀表达式 1
// 操作数直接输出,*比+的优先级高,所以直接入栈
st:[+ *] 后缀表达式 1 2
// 操作数直接输出,/和*的优先级一样高,*输出,/入栈
st:[+ /] 后缀表达式 1 2 3 *
// -比/的优先级低,所以/输出,-入栈
st:[+ -] 后缀表达式 1 2 3 * 4 /
//+ 和 - 的优先级相等,所以+输出
st:[-] 后缀表达式 1 2 3 * 4 / +
// 操作数输出,表达式遍历完毕,操作符全部出栈
st:[] 后缀表达式 1 2 3 * 4 / + 5 -
题目描述
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
示例
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]输出:
[null,null,null,null,-3,null,0,-2]解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
思路:
由于栈只能在尾部插入数据和删除数据,而队列是在尾部插入数据,在头部删除数据,所以我们可以使用两个栈,一个专门用来插入数据,一个用来专门出数据:
pushST:专门用来插入数据,当有元素入队列时直接插入pushST中
popST:专门用来出数据,当数据出队列时,检查popST是否为空,如果为空,将pushST中的全部数据全部取出插入到popST中–因为栈是先进后出的,所以当我们将pushST中的数据取出插入到popST后,原本位于栈底的数据就会位于栈顶,此时popST的出栈顺序就和队列的出队列顺序相同了
代码实现:
class MyQueue {
public:
MyQueue()
{}
// 检查popST是否为空,为空就将pushST的元素全部移动到popST中
void move()
{
if(popST.empty())
{
while(!pushST.empty())
{
popST.push(pushST.top());
pushST.pop();
}
}
}
// 直接插入到pushST中
void push(int x) {
pushST.push(x);
}
int pop() {
// pop之前先检查是否需要移动元素
move();
int x=popST.top();
popST.pop();
return x;
}
int peek() {
move();
return popST.top();
}
bool empty() {
// 两个栈均为空则为空队列
return pushST.empty()&&popST.empty();
}
private:
stack<int> pushST;
stack<int> popST;
};
【注意】
1.我们定义的pushST 和 popST是自定义类型,我们不写构造函数编译器会调用他们自身的构造函数,走初始化列表进行初始化,所以 MyQueue()函数里面可以不用写任何东西,此外,如果我们连 MyQueue()都不写,此时,编译器会自动生成一个无参的默认构造函数,它对于自定义类型同样也是自定义类型的默认构造,所以 MyQueue()不写或者 MyQueue()函数里面不写都是可以的
2.题目的接口中,peek,pop都需要先判断popST是否为空,如果为空就需要将pushST中的元素移动到popST中,为了避免代码的冗余,所以我们可以单独写一个函数来完成
三、queue
1.queue的介绍
queue和stack一样,也是一种容器适配器,不提供迭代器
cplusplus网址:queue文档介绍
【总结】
1.队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素
2.队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列
3.底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列
4.标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque
2.queue的使用
函数声明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
void queue_test()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
cout << q.size() << endl;
while (!q.empty())
{
cout << q.front() << " ";
q.pop();
}
cout << endl;
}
3.queue的模拟实现
queue.h
#pragma once
#include <vector>
#include <list>
#include <queue>
namespace hdp
{
//template<class T, class Container = list<T>>
template<class T, class Container = deque<T>>
class queue
{
public:
// 入队列
void push(const T& x)
{
_con.push_back(x);
}
// 出队列
void pop()
{
_con.pop_front();
}
// 获取队头元素
const T& top()
{
return _con.front();
}
// 判空
bool empty()
{
return _con.empty();
}
// 获取元素个数
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
test.cpp
#include <iostream>
#include <queue>
#include "queue.h"
using namespace std;
// 测试队列
void queue_test()
{
hdp::queue<int, deque<int>> q;
// 入队列
q.push(1);
q.push(2);
q.push(3);
q.push(4);
// 获取元素个数
cout << q.size() << endl;
while (!q.empty())
{
// 获取队头元素
cout << q.top() << " ";
// 出队列
q.pop();
}
cout << endl;
}
int main()
{
queue_test();
return 0;
}
4.queue的相关OJ题目
题目描述:
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶int pop()
移除并返回栈顶元素int top()
返回栈顶元素- boolean empty()
如果栈是空的,返回
true;否则,返回
false
注意:
- 你只能使用队列的基本操作 —— 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可
示例:
输入:
[“MyStack”, “push”, “push”, “top”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
思路
1.入数据:定义两个队列q1和q2,最开始入栈时随便将数据插入哪个队列,第二次及以后次插入数据时把数据插入到非空队列中
2.出数据:由于队列是先进先出而栈是后进后出,所以队尾的数据就是栈顶的数据,而出栈时我们需要将队尾前面的数据全部挪动到另一个空队列中,然后删除队列中剩余的那个数据,这样就完成了出栈的操作,此时这个队列就为空了,另一个队列不为空,下次再出栈的时候就可以把另一个不为空的队列除队尾的数据全部挪到这个空队列中,然后再删除队尾的数据
3.将q1中的数据挪到到q2中之后,数据的前后顺序是不会发生改变的,即入q1的数据为1 2 3 4 5 ,那么将数据取出插入到q2后,q2中的数据仍然为1 2 3 4 5 ,这是因为队列入队顺序和出队顺序是一样的
4.不进行出栈操作时,始终有一个队列为空,入栈直接插入到非空队列中,出栈则需要将非空队列的除队尾的数据全部挪动到另一个空队列中,然后再删除非空队列中剩余的那个数据
代码实现
class MyStack {
public:
// 不用显式写,编译器会调用queue的默认构造
MyStack() {
}
// 在非空队列中插入数据
void push(int x) {
if(!q1.empty())
{
q1.push(x);
}
else
{
q2.push(x);
}
}
// 相当于pop非空队列队尾的数据
int pop() {
if(q1.empty())
{
//队列中的数据大于1就继续挪动
while(q2.size()>1)
{
q1.push(q2.front());
q2.pop();
}
// 注意这里用top保存队尾的数据之后需要将那个数据pop掉
int top=q2.front();
q2.pop();
return top;
}
else
{
while(q1.size()>1)
{
q2.push(q1.front());
q1.pop();
}
int top=q1.front();
q1.pop();
return top;
}
}
// 非空队列的队尾数据即为栈顶数据
int top() {
if(!q1.empty())
{
return q1.back();
}
else
{
return q2.back();
}
}
// q1和q2均为空,则栈为空
bool empty() {
return q1.empty() && q2.empty();
}
private:
queue<int> q1;
queue<int> q2;
};
四、deque
1.deque的原理介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
2.deque的底层结构
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
【总结】
1.deque具有多个buffer数组,每个buffer数组可以存储n个数据,还具有一个存储buffer数组的中控指针数组,数组的每个元素都指向一个buffer数组
2.中控指针数组的使用,首先让数组最中间的元素指向第一个buffer,当第一个buffer数组存储满数据之后,再开辟第二个buffer数组,让指针数组的后一个位置或前一个位置指向新开辟的数组,当一个buffer数组满了之后,我们头插数据的时候是在前面开辟一个数组且数据放在前一个buffer数组的最后一个位置,尾插则是放在后一个buffer数组的第一个位置
3.关于deque的扩容机制,当指针数组满了指针,会让中控数组的容量变为原来的二倍,然后将原来中控数组里的数据拷贝到新的中控指针数组中
3.deque的迭代器设计
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
4.deque的缺陷
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
我们总结为具有vector的优点:支持随机访问,CPU高速缓存命中率较高,尾部插入删除数据的效率高,同时具有list的优点:空间浪费少,头部插入数据的效率高
但是,deque有一些致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。此外,deque的随机访问的效率较低,其访问过程为–首先通过中控数组的指针找到对应的buffer数组,然后再找到具体的位置,我们假设每个buffer中有10个元素,假设偏移量为i,需要i/10得到位于第几个buffer数组,然后再i%10得到buffer数组中的具体位置。并且,deque在中部插入删除数据的效率也比较低,因为需要挪动数据,但不一定后续buffer数组中的数据全部挪动,可以控制只挪动一部分,即中间插入删除数据的效率高于vector,但低于list
为什么选择deque作为stack和queue的底层默认容器
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
1.stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
2.在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。结合了deque的优点,而完美的避开了其缺陷
deque使用场景
deque适用于需要大量进行头插和尾部数据的插入删除,偶尔随机访问,偶尔进行中部插入删除的场景,不太适合需要大量进行随机访问与中部数据插入删除的场景,还有排序