今天刷题学会了单调栈的使用,记录一下。
题目
暴力写法(会超时)
一开始读完题目,我想着以数组每一个值为左边界,然后滑动右边界找。类似于滑动窗口?如果大于左边界就停止循环找;如果等于就把结果res+1,然后继续往后找;实现如下
//暴力写法:会超时
long long numberOfSubarrays(int* nums, int numsSize) {
/*从数组的第一个开始,从j=i,(j<n)滑动窗口,如果a[i]==a[j](res++)||a[i]>a[j],j++;
如果a[j]>a[i],则以数组下一个以left,开始滑动窗口。
*/
int i,left,right;
int res=0;//结果
for(i=0;i<numsSize;i++)
{
left=i;
for(right=left;right<numsSize;right++)
{
if(nums[left]==nums[right])
{
res++;
}
if(nums[right]>nums[left])
{
break;
}
}
}
return res;
}
思路简单,但是会超时。
所以我认为这道题的难点,就是要找到一种减少循环的次数的方法。
单调栈写法
看这个例子,1,4,3,3,2,1。1后面是4,所以哪怕后面再遇到1,也不可能组成边界是最大值的子串了,所以这个1就是无用的了。那么如果我使用一个栈,栈的每一项会保存数组里的值val和这个值出现的次数cnt,并且栈是单调递减的。做如下操作:
令返回的结果为res,res=numsSize(数组的每一项都是满足条件的子串)
遍历原数组的每一项,x=nums[i]:
只要x>栈顶:则把栈顶出栈
如果x<栈顶:入栈,并把该值出现的次数设为1;
如果x==栈顶:res+=栈顶元素的cnt,(因为前面有几个和x相等的值,就会和这个x组成几个满足条件的子串);cnt++;
遍历完之后,就能得到答案,而且循环次数大大减少!
利用单调栈的优越性体现在哪?
如果是上述暴力解法,数组每一项都要再遍历数组,二层循环必须执行的。
但是如果是用单调递减的栈,那么只有在x大于栈顶的情况下会有第二层循环,
而且是在一个在节点数比原数组少的栈里循环(因为我们在维护单调栈时:与栈顶相同不入栈,只增加次数;而且在之前的节点x>栈顶时,会出栈一些节点),所以二层循环节点大大减少。
为了代码书写简洁,刚开始我入栈了一个val为正无穷(int类型的正无穷用INT_MAX表示)的值。保证数组的第一个值能成功入栈。
以下是我的实现代码:
//定义一个结构体,保存值和其出现的次数
typedef struct node
{
int val;//value
int cnt;//count
}node;
#define Max_lenth 100001
long long numberOfSubarrays(int* nums, int numsSize) {
node stack[Max_lenth];//栈
int top=0;//栈顶指针
stack[top].val=INT_MAX;
stack[top++].cnt=0;
unsigned long long res=numsSize;
for(int i=0;i<numsSize;i++)
{
int x=nums[i];
//大于,取代
while(stack[top-1].val<x)
{
top--;
}
if(x<stack[top-1].val)//小于栈顶,入栈
{
stack[top].val=x;
stack[top++].cnt=1;
}
else if(x==stack[top-1].val)//等于栈顶,栈顶次数加1
{
res+=stack[top-1].cnt;
stack[top-1].cnt++;
}
}
return res;
}
总结
这道题使用栈来解决真的蛮巧妙的,如果直接暴力求解,对于数组的每一个元素都要再有一个循环来找它的后面还有没有节点与之组合成符合条件的子数组。但是用栈就可以把当前元素的之前元素保存下来,一边遍历,一边判断,使用空间来节省时间。