数据结构 —— 栈(stack)在算法思维中的巧妙运用
栈是数据结构中重要的成员,它特殊的结构使它有非常多的用法,在算法中有时会起到意想不到的作用。这篇文章就主要介绍栈在算法中的神奇用法~
栈的特点就是先进后出,能较灵活的处理当前的数据。而单调栈,顾名思义就是使得栈中元素是单调的,这在特定的题中有非常好的优化效果。具体的实现思路看下面这道模板题:
题目:496. 下一个更大元素 I - 力扣(LeetCode)
常规思路就是直接拿num1的元素到num2里面暴力查找,这样就是一个O(nm) 的复杂度。如果此时现对num2进行预处理并将相应的值用map进行映射,那么到num1中直接O(1)查表就行了。那么如何在num2中高效查到第一个更大的元素呢?
这个时候栈的特点就显现出来了。由于栈是后进先出,刚好与题中要求的其后更大的元素特点相吻合,当遍历到此元素时,直接进行对栈中元素进行查找,找到第一个大于的就行。
class Solution {
public:vector<int> nextGreaterElement(vector<int>& n1, vector<int>& n2) {unordered_map<int, int> m;vector<int> v;stack<int> st;for(int i=n2.size()-1; i>=0; i--)//从后向前进行遍历{int n=n2[i];//将当前元素给提出来while(!st.empty()&&n>st.top())//一直弹出,直到找到比当前更大的元素st.pop();if(st.empty()) //这说明后面没有比它更大的元素,根据题意赋-1m[n]=-1;else m[n]=st.top();st.push(n);}for(auto i:n1) v.push_back(m[i]);//num1直接查表就行了return v;}
};
题目:739. 每日温度 - 力扣(LeetCode)
class Solution {
public:vector<int> dailyTemperatures(vector<int>& t) {unordered_map<int, int> m;vector<int> v(t.size());stack<int> s;for(int i=t.size()-1; i>=0; i--){int n=t[i];while(!s.empty()&&t[s.top()]<=n)s.pop();if(!s.empty()) v[i]=s.top()-i;s.push(i);}return v;}
};
这两道题思路是一样的,不一样的就是一个是让直接求值而另一个是求距离。
括号匹配问题可以说是栈的题型中相当经典的了,下面两道就是关于括号匹配的问题。
题目:856. 括号的分数 - 力扣(LeetCode)
题目大意:
题目非常短,我第一次看的时候看了半天没太看懂,稍微解释一下。如果是空括号的话就是1分,如果括号存在包含关系的话就是内部的分数✖️2,如果是并列关系的话直接加起来就可以了。题目保证所有括号一定可以匹配得上。
解题思路:
大概的思路和常规的是一样的,就是当左括号的时候直接压入栈中,是右括号的话对当前状态进行结算。具体来说的话,此题总分数是一步步从右向左传递相加得到的。当碰到右括号的时候需要把当前的分数传给前面那个左括号。所以在最开始需要先压入一个0用来接收总分数,后面每遍历到一个左括号都需要压入一个0来接收后面括号对的分数。当遇到右括号的时候就面临两种情况:到底是空括号还是非空括号。如果是空括号的话此时最近的左括号代表的值一定是0,即中间没有任何表达式给它们传分数。按照规则这是1分。所以可以简单将此步骤进行合并为: st.top() += max (1,v*2)。经过层层的传递剩下的栈顶就是总分数了~
//当前括号的值传给前面的
class Solution {
public:int scoreOfParentheses(string s) {stack<char> st;st.push(0);//所有括号都会执行并被弹出,多压一个用来表示结果for(auto c:s){if(c=='(') st.push(0);else{int v=st.top();st.pop();st.top()+=max(1,v*2);//如果是空括号的话算1分,有东西的话*2}}return st.top();}
};
题目:32. 最长有效括号 - 力扣(LeetCode)
解题思路:
刚开始写的时候感觉这两道题非常的奇怪难懂,再看一遍发现是有一些相似之处的。此题是当到右括号进行结算的时候,此时的距离就是这个右括号和上一个右括号的距离并将此时的左括号给弹出来。这样的话如果中间没有错误的括号那么本次括号匹配并不会影响到后面右括号对前面进行匹配。比如:)()()右括号本来就不入栈,第一次括号匹配的时候将左括号给弹出来了,此时两个右括号之间的距离是2,刚好就是一对括号的距离,变成了 )(),到第二次括号匹配的时候,此时的右括号还是找最前面那个括号,就将本次的距离也给加上了。像 )((())) 这种情况也同样适用。还有一个需要注意并且有些相似的是两个题都需要提前塞进去一个东西。本题是提前塞进去一个右括号(的下标)做一个基准。说了这么多看下代码应该可以更好理解:
class Solution {
public:int longestValidParentheses(string s) {stack<int> st;int ans=0;st.push(-1);//例如(),1-(-1)=2,这样就可以准确计算括号距离了for(auto i=0; i<s.size(); i++){if(s[i]=='(') st.push(i);//将下标推入if(s[i]==')'){st.pop();if(st.empty()) st.push(i);elseans=max(ans,i-st.top());}}return ans;}
};
压轴:经典题目 接雨水
42. 接雨水 - 力扣(LeetCode)
为什么说这题经典,一个是非常的形象巧妙,还有就是之前甚至在某音都刷到过这题😄本题可以用动态规划和栈两种方法进行解答。
先说一下栈的思路:
这个题我是放在这几道题的最后写的,想了好久其实是想到了用栈。因为顺着前面几道题的规律,我发现本题与匹配括号思路有些相似,当高度呈递增的时候才能形成一个完成的水坑进行结算。这与前面匹配括号时不用管左括号,只有当遇到右括号才进行结算的思想恰恰谋和。但是当时思维有点局限,总想着要一次把一个水坑的面积给求出来,没曾想水坑也可以从下到上一层层求。题目非常的形象有意思,接下来看下代码。
class Solution {
public:int trap(vector<int>& h) {stack<int> s;int sum=0;int n=h.size();//1 0 2 1 0 1 3 2 1 2 1for(int i=0; i<h.size(); i++){while(!s.empty()&&h[s.top()]<h[i])//当呈递增的时候(有点单调栈的思想){int top=s.top();s.pop();//要弹出是因为想让左边那个也露出来if(s.empty()) break;int left=s.top();int width = i-left-1;//此时的宽度int hight = min(h[i],h[left])-h[top];//此时的宽度sum+=width*hight;}s.push(i);}return sum;}
};
补充:水坑计算的大概过程:每上升一次就对当层进行计算,此水坑需要加两次计算完成。
接下来说一下动态规划的思路:
我当时看题解的时候光看到那个图就懂了哈哈,但是细细想来如果真让自己写还真是不好想。那么我就放一下图吧:
分别从两边各进行一次线性DP,取较小值与原高度相减。
class Solution {
public:int trap(vector<int>& h) {const int N = 2e4+5;vector<int> leftmax(N),rightmax(N);int size=h.size();int sum=0;leftmax[0]=h[0],rightmax[size-1]=h[size-1];for(auto i=1; i<size; i++)leftmax[i]=max(leftmax[i-1],h[i]);for(auto i=size-2; i>=0; i--)rightmax[i]=max(rightmax[i+1],h[i]);for(int i=0; i<size; i++){int x=min(leftmax[i],rightmax[i])-h[i];sum+=x;}return sum;}
};
好了,这篇博客就到这了,如果还有什么运用方法欢迎交流~