单调栈
这道题就是单调栈最经典的应用,单调栈往往就是求一个数组中每个元素左边(之前)的元素中离它最近的比它小的元素。
首先很容易想到暴力做法,对于每一个元素,从当前元素的前一个元素开始往前遍历,第一个满足小于当前元素的元素就是答案,遍历完数组都不存在元素比当前元素小,则输出-1.
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N];
int main() {
int n;
cin >> n;
for(int i = 0; i < n; ++i) {
cin >> a[i];
}
for(int i = 0; i < n; ++i) {
int j = i - 1;
for( ; j >= 0; --j) {
if(a[j] < a[i]) {
break;
}
}
if(j == -1) {
cout << -1 << ' ';
} else {
cout << a[j] << ' ';
}
}
cout << endl;
}
暴力做法有两层循环,每一个元素都有可能向前遍历当前元素下标的大小,时间复杂度是O(n^2)。
暴力法可以这样理解,遍历每一个元素都压入栈中,对于当前遍历到的下标i,可以认为有个栈,从栈底到栈顶分别为nums[0]~nums[i - 1],所以我们要找i左边离它最近的比它小的元素,就可以一直和栈顶元素对比,如果栈顶元素大于等于nums[i],就一直弹出栈顶元素,直到栈空或者找到一个元素小于nums[i],这个元素必然就是第一个比nums[i]小的元素。
考虑一下优化,对于j < i,如果nums[j] > nums[i],则我们寻找i之后的元素的最近的较小元素时,nums[j]是不可能作为答案的,因为i的下标比j大,且nums[i]小于nums[j],所以nums[i]才有可能是备选答案,nums[j]肯定不是备选答案。
单调栈的思想是这样的:我们可以遍历一遍原数组并维护一个栈,每个元素都和当前栈顶元素进行比较,如果当前元素大于栈顶元素,说明之前记录了比当前元素小的且离当前元素最近的元素(栈顶元素),输出栈顶元素,且现在这个元素不入栈。也可以这么理解,我们当前元素比之前的栈顶元素更靠右,且值更小,所以之后的数要找比它们小的最靠近的数,肯定不可能是当前的栈顶元素,而有可能是我们现在遍历到的这个下标的元素,那我们为什么还要在栈里存这些元素呢?所以我们就一直把栈顶元素弹出,直到栈空或者栈顶元素小于当前元素。栈顶元素小于当前元素就不要出栈了,很好理解,因为之后的元素可能比当前元素小,但还是比在这之前的元素(栈顶元素或者更小的元素)要大,所以之前的较小元素还有可能是备选答案。因此留在栈内。
所以我们只需要维护一个单调递增的栈(单调栈的名称就是这样由来的),就可以知道每个元素往左边第一个比他小的元素。
单调栈和暴力法比优化在哪呢?
首先只对元素遍历一遍,对于每个元素,寻找栈内第一个比他小的数,如果没找到就一直弹出栈顶元素(直到栈空或找到),弹出的元素不可能再加入栈了。
所以每一个元素只有可能入栈和出栈一次。因此时间复杂度其实是O(n)的,而暴力的复杂度是O(n^2)。
单看文字可能有些难以理解,结合代码模拟一下就容易懂了。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int stk[N], top = 0; //用数组模拟栈,top是栈顶元素指针
int main() {
int n;
cin >> n;
for(int i = 0; i < n; ++i) {
int x;
cin >> x;
while(top > 0 && stk[top] >= x) { //如果栈非空,且栈顶元素大于等于当前元素,说明栈顶元素不可能是之后元素的备选答案了,出栈!
--top;
}
if(top > 0) { //经过上面的while循环,如果栈非空,则top指针指向的栈顶元素就是当前元素的答案(左边第一个比它小的数)
cout << stk[top] << ' ';
} else { //如果栈为空(top == 0),说明当前元素左边的所有元素都大于等于它,输出-1
cout << -1 << ' ';
}
stk[++top] = x; //将当前元素加入栈中
}
}