文章目录
关于单调栈:
关键词:下一个,上一个,最近,更大,更小
如果要找数组中比当前值大的最近值,就用单调递减栈,反之就用单调递增栈。这里需要注意相等的情况,视题目而定,看说的是大于还是不小于。
1、每日温度(单调栈)
题目
分析
这题其实如果想到用单调栈了的话就不难了,构建一个单调递减栈从后往前遍历,然后为了求距离还需要一个map来记录温度所在的位置。如果当前的温度比栈顶温度高就出栈,一直到栈为空或者找到比当前温度更高的温度为止,然后计算距离或者栈空时输出0,代码如下:
vector<int> dailyTemperatures(vector<int>& T) {
int n=T.size();
if(n==0) return {};
stack<int> s;
vector<int> out(n,0);
unordered_map<int,int> pos;
for(int i=n-1;i>=0;--i){
pos[T[i]]=i;
while(!s.empty()&&s.top()<=T[i]) s.pop();
if(!s.empty()){
out[i]=pos[s.top()]-i;
}
else{
out[i]=0;
}
s.push(T[i]);
}
return out;
}
复杂度
时间复杂度:O(N)
空间复杂度:O(N)
2、下个更大元素Ⅰ/Ⅱ
题目
分析
这个两个题目都是用单调递减栈。
第一题是两个数组,而nums1是nums2的子集,所以可以先遍历nums2然后用map记录,再遍历一遍nums1,并从map中读值填入vector输出。
而第二题在一的基础上增设了循环数组的条件,但只有一个数组,所以这里不再需要用map了。
对循环数组的处理方式是在原数组的后面接上一个原数组,使其翻倍。这里有个技巧,我们可以不将它真的翻倍,而是运用%来进行模拟,如下所示:
for(int i=2*n-1;i>=0;--i){
int k=i%n;
while(!s.empty()&&s.top()<=nums[k]) s.pop();
if(s.empty()) out[k]=-1;
else out[k]=s.top();
s.push(nums[k]);
}
复杂度
时间复杂度:O(N)
空间复杂度:O(N)
3、接雨水
题目
分析
参考:link
首先要知道按列求该怎么做,我们需要找出当前列的左边和右边的最高墙,然后用其的较小值减去当前列的高度即可得到当前列能接到的雨水量了,如下图:
用空间换时间:
我们可以先把每一列的左边和右边的最高墙先记录下来,然后求的时候直接取用,如下:
vector<int> max_left(n,0),max_right(n,0);
for(int i=1;i<n;++i){
max_left[i]=max(max_left[i-1],height[i-1]);
}
for(int j=n-2;j>=0;--j){
max_right[j]=max(max_right[j+1],height[j+1]);
}
这里要注意求的时候不能算上当前列。
单调栈:
这里维护一个单独递减栈,然后从左向右遍历,如果当前列的高度大于栈顶高度,说明该列会有积水,于是出栈并计算积水量,如下:
stack<int> s;
s.push(0);
int rain=0;
for(int i=1;i<n;++i){
if(height[i]>height[s.top()]){
while(!s.empty()&&height[i]>height[s.top()]){
int cur=s.top();
s.pop();
if(!s.empty())rain+=min(max_left[cur],max_right[cur])-height[cur];
}
}
s.push(i);
}
注意,这里因为要用到max_left和max_right,所以存入栈的应该是下标。
当然,这里不用单调栈直接遍历来做也可以,如下:
int rain=0;
for(int i=0;i<n;++i){
int minhigh=min(max_left[i],max_right[i]);
if(minhigh>height[i])rain+=minhigh-height[i];
}
这里minhigh>height[i]的检查必不可少,参考题目给的例子的第八个数(3),也即最大值,如果没有这个检查,那么它这里会减1,因为它左右最高的都是2。同样第四个数(2),因为其左边最高为1,所以它这里也会减1。
复杂度
时间复杂度:O(N)
空间复杂度:O(N)
空间优化方法:
参考:link
这个做法涉及到数学方面的东西:
首先,从左往右遍历,不管是雨水还是柱子,都计算在有效面积内,并且每次累加的值根据遇到的最高的柱子逐步上升。面积记为S1。
然后,从右往左遍历可得S2。
最后,直接以最长柱子为高求整个矩形面积,观察发现,S1 + S2会覆盖整个矩形,并且:重复面积 = 柱子面积 + 积水面积
所以,可得积水面积 = S1 + S2 - 矩形面积 - 柱子面积。
代码如下:
int trap(vector<int>& height) {
int n=height.size();
int maxhei=0;
int left=0,right=0;
for(int i=0;i<n;++i){
maxhei=max(height[i],maxhei);
left+=maxhei;
}
maxhei=0;
for(int i=n-1;i>=0;--i){
maxhei=max(height[i],maxhei);
right+=maxhei;
}
int rectangle=maxhei*n;
int pillar=accumulate(height.begin(),height.end(),0);
return left+right-rectangle-pillar;
}
复杂度
时间复杂度:O(N)
空间复杂度:O(1)
4、最长的有效括号
题目
分析
参考:link
思路:
我们先找到所有可以匹配的索引号,然后找出最长连续数列!
例如:s = )(()()),我们用栈可以找到,
位置 2 和位置 3 匹配,
位置 4 和位置 5 匹配,
位置 1 和位置 6 匹配,
这个数组为:2,3,4,5,1,6 这是通过栈找到的,我们按递增排序!1,2,3,4,5,6
找出该数组的最长连续数列的长度就是最长有效括号长度!
子串和子序列的区别:
给定 "pwwkew" ,
子串是pww,wwk等很多个子串 是连在一起的
子序列是 pwk,pke等很多个子序列 ,但是子序列中的字符在字符串中不一定是连在一起的。
这里需要注意的是要求的最长子串的长度,并没有要求直接把最长子串写出来,所以我们这里还是可以用正常的栈模式求解,只不过这里我们要记录的是括号的下标,然后在弹出时用一个vector记录下来,再将vector排序,最后统计出最长的连续子串长度,代码如下:
int longestValidParentheses(string s) {
int n = s.size();
if (n < 2) return 0;
stack<int> tmp;
vector<int> posnums;
for (int i = 0; i < n; ++i) {
if (s[i] == ')' && !tmp.empty()) {
posnums.push_back(i);
posnums.push_back(tmp.top());
tmp.pop();
}
else if (s[i] == '(') tmp.push(i);
}
sort(posnums.begin(), posnums.end());
int maxlen = 0;
int left = 0, right = 0;
while (right < posnums.size()) {
left = right;
while (right < posnums.size()-1&&posnums[right] == (posnums[right+1] - 1)) ++right;
maxlen = max(maxlen, right - left+1);
++right;
}
return maxlen;
}
复杂度
时间复杂度:O(N)
空间复杂度:O(N)
5、逆波兰表达式求值
题目
分析
这题就是道基础的栈题目,代码如下:
int evalRPN(vector<string>& tokens) {
int n=tokens.size();
stack<int> s;
for(int i=0;i<n;++i){
if(tokens[i]!="+"&&tokens[i]!="-"&&tokens[i]!="*"&&tokens[i]!="/"){
s.push(atoi(tokens[i].c_str()));
}
else{
int s1=s.top();
s.pop();
int s2=s.top();
s.pop();
switch (tokens[i][0])
{
case '+':
s.push(s1+s2);
break;
case '-':
s.push(s2-s1);
break;
case '*':
s.push(s1*s2);
break;
case '/':
s.push(s2/s1);
break;
default:
break;
}
}
}
return s.top();
}
6、基本计算器
题目
分析
这题本身并不难,就是单纯的栈处理,但是因为它里面有很多特别的情况,使得这个题目显得很麻烦。先上代码:
int calculate(string s) {
int n = s.size();
if (n == 0) return 0;
stack<char> books;
int i = 0;
while (i < n) {
if (s[i] == ' ') {}
else if (s[i] == ')') {
string a = "";
while (books.top() != '(') {
a += books.top();
books.pop();
}
books.pop();
reverse(a.begin(), a.end());
int m = a.size();
int j = 0;
int num = 0;
int sym = 1;
while (j < m) {
string b = "";
while (j<m&&a[j] != '+'&&a[j] != '-') {
b += a[j];
++j;
}
int c = atoi(b.c_str());
num += sym * c;
if (j < m&&a[j] == '+') sym = 1;
else if (j < m - 1 && a[j] == '-'&&a[j + 1] == '-') {
sym = 1;
++j;
}
else if (j < m&&a[j] == '-') sym = -1;
++j;
}
string s2 = to_string(num);
for (int k = 0; k < s2.size(); ++k) books.push(s2[k]);
}
else {
books.push(s[i]);
}
++i;
}
string s3 = "";
while (!books.empty()) {
s3 += books.top();
books.pop();
}
reverse(s3.begin(), s3.end());
int m = s3.size();
int j = 0, ret = 0, sym = 1;
while (j < m) {
string b = "";
while (j<m&&s3[j] != '+'&&s3[j] != '-') {
b += s3[j];
++j;
}
int c = atoi(b.c_str());
ret += sym * c;
if (j < m&&s3[j] == '+') sym = 1;
else if (j < m - 1 && s3[j] == '-'&&s3[j + 1] == '-') {
sym = 1;
++j;
}
else if (j < m&&s3[j] == '-') sym = -1;
++j;
}
return ret;
}
这里面在处理括号时,由于要分段来处理,所以要特别小心不能把字符写错,这也是里面很麻烦的一点,因为要设很多的变量,写的时候要小心。
第二点,例如:
"2-(5-6)"
这里将括号处理完后会得到,
2--1
由于连着两个负号应该记为 + ,所以这里要加一层判断,
else if (j < m - 1 && s3[j] == '-'&&s3[j + 1] == '-') {
sym = 1;
++j;
}
7、柱状图中最大的矩形
题目
分析
参考:link
这题较为繁琐,题解很长,就不一一记下来了。重点是两点:
1)使用单调递增栈
2)栈中保存下标
直接上代码,
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
if (n == 0) return 0;
stack<int> sk;
sk.push(0);
int i = 1, maxarea = 0;
for (; i < n; ++i) {
if (heights[i] >= heights[sk.top()]) {}
else {
while (!sk.empty() && heights[sk.top()] > heights[i]) {
int cur = sk.top();
sk.pop();
int j=-1;
if(!sk.empty()) j=sk.top();
int area = (i - j-1)*heights[cur];
maxarea = max(maxarea, area);
}
}
sk.push(i);
}
while (sk.size() > 1) {
int cur = sk.top();
sk.pop();
int j=-1;
if(!sk.empty()) j=sk.top();
int area = (i - j-1)*heights[cur];
maxarea = max(maxarea, area);
}
maxarea = max(maxarea, heights[sk.top()] * n);
return maxarea;
}
这里重点注意对area宽的计算,也就是 (i - j-1) 这部分,最开始我错误的使用了(cur-i),为什么这样做是错误的,见下面例子,
[5,4,1,2]
高度4对应的宽应该是2,但(cur-i)得到的结果是1,而且在出栈时,栈中元素只剩它自己,所以这里 j 首先赋值为 -1,就是为了应对出栈后栈为空的情况。
复杂度
时间复杂度:O(N)
空间复杂度:O(N)
8、最小栈&最大队列
分析
最小栈
参考:link
这里要使用一个辅助栈,用法如下:
(1)辅助栈为空的时候,必须放入新进来的数;
(2)新来的数小于或者等于辅助栈栈顶元素的时候,才放入。、
注意:这里“等于”要考虑进去,因为出栈的时候,连续的、相等的并且是最小值的元素要同步出栈;
(3)出栈的时候,辅助栈的栈顶元素等于数据栈的栈顶元素,才出栈。
这么做首先要明白,辅助栈的栈底与数据栈是相同的,这就保证了只要数据栈不空,辅助栈就不会空。而它只入比栈顶小的元素,这就满足了题目要求的常数时间求最小值的需求,同时在出栈后留下的也是剩下的元素中的最小值。
代码不难,这里就不贴了。
最大队列
参考:[link](https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/solution/ru-he-jie-jue-o1-fu-za-du-de-api-she-ji-ti-by-z1m/)
这里使用了双向队列为辅助,双向队列维护成递减的形式,这就保证了最大值出队后,余下的是第二大的值,同时因为队列的顺序性,最后一个值肯定是会在队列中的,这点很重要。
代码如下:
class MaxQueue {
public:
queue<int> a;
deque<int> b;
public:
MaxQueue() {
}
int max_value() {
if(a.empty()){
return -1;
}
return b.front();
}
void push_back(int value) {
while(!b.empty()&&value>b.back()){
b.pop_back();
}
b.push_back(value);
a.push(value);
}
int pop_front() {
if(a.empty()){
return -1;
}
int cnt=a.front();
if(!b.empty()&&cnt==b.front()){
b.pop_front();
}
a.pop();
return cnt;
}
};