数据结构和算法之栈+队列
栈
先进后出的数据结构,思想比较简单,STL要熟练运用才行。没有迭代器,主要就是push和pop操作。
单调栈
介绍:
- 一种特殊的栈,要求栈中的元素是单调递增或者单调递减的
- 单调递增栈:一般栈中被弹出的元素都是大于当前元素的,因此当前元素就是第一个小于栈中某些元素的最近的元素。
实际使用:
- 题目:一般求解下一个(第一个)大于…或者下一个(第一个)小于…的这种题目,就是找到在当前值之后的第一个大于它或者第一个小于它的元素的位置
算法流程:
- 如果压栈之后保持单调性就直接压栈,否则先出栈直到可以压入元素保持单调性
哨兵技巧
考虑所有的数组信息,可以将所有元素出栈。
以下面这个单调递增栈例子开始:
一个数组num=[1,3,4,5,2,9,6],可以在其右侧添加一个小于数组最小值的项,例如-1,变成数组[1,3,4,5,2,9,6,-1]。使用ans表示在其之后第一个小于其本身的位置,ans[i]表示num[i]之后第一个小于num[i]的位置,ans[i]=-1表示这样的位置不存在。算法流程就是:
- 首先压入1,此时栈为[1],i=0,ans[i]=-1;
- 继续压入3,此时栈为[1,3],i=1,ans[1]=-1;
- 继续压入4,此时栈[1,3,4],i=2,ans[2]=-1;
- 继续压入5,此时栈为[1,3,4,5],i=3,ans[3]=-1;
- 此时i=4,num[i]=2,小于栈顶元素,不满足单调栈的性质,联想栈的操作,要pop元素直到满足单调栈的性质。所以此时:一直弹出栈中的元素直到栈顶元素小于当前的num[i];而弹出的所有元素num[1],num[2],num[3]它们的之后第一个小于它的元素就是此时的num[i]。此时栈为[1,2],ans[1],ans[2],ans[3]都为i的下标4。
- 继续压入9,此时栈为[1,2,9],i=5,ans[5]=-1;
- 此时i=6,num[i]=6,小于栈顶元素,不满足单调栈的性质,同理也是pop元素直到满足单调栈的性质。ans[6]=-1;
- 此时到了最后一个元素-1,单调递增栈的性质所以栈中所有元素都要出栈了。
做题的时候还是要理解题意,有的题目前后加上哨兵也行,解法很多,但是要深刻理解题目的意思,最好结合图示理解,
模板题目:
题目是这样的,给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。
简单的例子:
input: 5,3,1,2,4
return: -1 3 1 1 -1
explaination: 对于第0个数字5,之后没有比它更大的数字,因此是-1,对于第1个数字3,需要走3步才能达到4(第一个比3大的元素),对于第2和第3个数字,都只需要走1步,就可以遇到比自己大的元素。对于最后一个数字4,因为之后没有更多的元素,所以是-1。
class Solution {
public:
int trap(vector<int>& height) {
stack<int> stk;
int n = height.size();
vector<int>ans(n,-1);//表示在其之后第一个小于其本身的位置,初始化-1,和heighr长度一样
for (int i = 0; i < n; ++i) {
while (!stk.empty() && height[i] < height[stk.top()]) {
int top = stk.top();
stk.pop();
//一番具体操作...
ans[top]=i-top;
stk.push(i);
}
return ans;
}
};
以leetcode这个题目为例子,深刻理解下单调栈的应用。
leetcode84
- 首先第一步如何确定这个题目要使用单调栈?:我们遍历或者说缓存数据的时候是从左向右,而我们又需要从右向左来进行操作,并且操作一个元素之后一个元素就可以舍弃,这就符合栈的特点,同时我们要保证栈中的元素是递增或者递减,那我们就用单调栈。关于这道题目:可以在数组前后都加上哨兵元素,也可以在数组后面加上哨兵元素。
关于数组前加上哨兵元素是为了避免栈为空的时候的判断,数组后面加上哨兵元素是为了让所有的元素全都计算过。
而且vector也可以当作栈。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//先判断用单调递增还是递减栈
heights.push_back(-1);
stack<int>st;
int n=heights.size();
int ans=0;
for(int i=0;i<n;i++){
while(!st.empty()&&heights[st.top()]>=heights[i]){
int h=heights[st.top()];
st.pop();
int sidx=0;
//画图理解比较好,因为是先弹出st,所以要判断下是否有0
if(st.empty()){
sidx=-1;
}
else sidx=st.top();
//同理因为先弹出的元素,所以这时候st.top就是上一个元素了,所以b=i-sidx-1.
int broad=i-sidx-1;
ans=max(ans,h*broad);
}
st.push(i);
}
return ans;
}
};
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//先判断用单调递增还是递减栈
heights.insert(heights.begin(),-1);
heights.push_back(-1);
stack<int>st;
int n=heights.size();
int ans=0;
st.push(0);
for(int i=0;i<n;i++){
while(heights[st.top()]>heights[i]){
int h=heights[st.top()];
st.pop();
int broad=i-st.top()-1;
ans=max(ans,h*broad);
}
st.push(i);
}
return ans;
}
};
队列
一段进一段出的数据结构,STL同样要熟练运用才行。
优先队列(堆)
介绍:
一般用来维护前k个元素的时候可以使用,和队列的区别在于,先进队列的不一定先出,而是优先级最大的元素先出队列。这个优先级是根据具体问题来的。STL默认是大顶堆。
STL
template <typename T,
typename Container=std::vector<T>,
typename Compare=std::less<T> >//greater<T>是元素值从小到大排序。
class priority_queue{
//......
}
三个参数:指定的元素类型,指定使用的底层容器,指定排序规则。
自定义排序方法:
可以重载operater<或者重载operatre>或者重写仿函数cmp
struct Node{
int x;
int y;
Node(int a,int b):x(a),y(b){}
};
//重载operator<:为自定义结构体创建大顶堆(降序)
bool operator<(Node a,Node b){
if(a.x==b.x)return a.y<b.y;
return a.x<b.x;
}
priority_queue<Node,vector<Node>,less<Node>>qu;
//重写仿函数
struct cmp{
bool operator()(T a,T b){
return a<b;
}
}
priority_queue<Node,vector<Node>,cmp>qu;
几个熟记的点:
- 和 queue 一样,priority_queue 也没有迭代器,因此访问元素的唯一方式是遍历容器,通过不断移除访问过的元素,去访问下一个元素
- 常用函数:top()区别于queue的front():返回容器的第一个元素的引用
使用:
堆排序:
//堆STL使用,优先队列
//priority_queue,每次只能访问位于队头的元素。
//先进队列的不一定先出,而是优先级最大的元素先出队列。默认是大顶堆。
//STL自定义排序
struct cmp{
bool operator()(int a,int b){
return a>b;//>对应greater,升序排列,小顶堆
}
};
vector<int> stl_heapSort(vector<int>&arr){
priority_queue<int,vector<int>,cmp>q(arr.begin(),arr.end());//
int n=arr.size();
vector<int>result;
while(!q.empty()){//priority_queue没有迭代器,所以只能通过移除一个个元素来进行访问
result.push_back(q.top());//普通队列是q.front();
q.pop();
}
//reverse(result.begin(),result.end());
return result;
}
经典题目: