单调栈要求栈中的元素是单调递增的或者单调递减的。
- 单调递增(不减)栈可以找到左边第一个比当前出栈元素小(包含等于)的元素。
- 单调递减(不增)栈可以找到左边第一个比当前出栈元素大(包含等于)的元素。
是否严格递增或递减可以根据实际情况来。
以左边为栈底,右边为栈顶为例:
- [1,2,3] 就是一个单调递减栈(因为此时的出栈顺序是 3,2,1。下同,不再赘述)
- [3,2,1] 就是一个单调递增栈
- [1,3,2] 就不是一个合法的单调栈
单调栈适合的题目是求解下一个大于 x 或者下一个小于 x 的问题。
比如我们需要依次将数组 [1,3,4,5,2,9,6] 压入单调栈。
- 首先压入 1,此时的栈为:[1]
- 继续压入 3,此时的栈为:[1,3]
- 继续压入 4,此时的栈为:[1,3,4]
- 继续压入 5,此时的栈为:[1,3,4,5]
- 如果继续压入 2,此时的栈为:[1,3,4,5,2] 不满足单调递减栈的特性, 因此需要调整。如何调整?由于栈只有 pop 操作,因此我们只好不断 pop,直到满足单调递减为止。
- 上面其实我们并没有压入 2,而是先 pop,pop 到压入 2 依然可以保持单调递减再 压入 2,此时的栈为:[1,2]
- 继续压入 9,此时的栈为:[1,2,9]
- 如果继续压入 6,则不满足单调递减栈的特性, 我们故技重施,不断 pop,直到满足单调递减为止。此时的栈为:[1,2,6]
注意这里的栈仍然是非空的,如果有的题目需要用到所有数组的信息,那么很有可能因没有考虑边界而不能通过所有的测试用例。 这里介绍一个技巧 - 哨兵法,这个技巧经常用在单调栈的算法中。
对于上面的例子,我可以在原数组 [1,3,4,5,2,9,6] 的右侧添加一个小于数组中最小值的项即可,比如 -1。此时的数组是 [1,3,4,5,2,9,6,-1]。这种技巧可以简化代码逻辑,大家尽量掌握。
上面的例子如果你明白了,就不难理解为啥单调栈适合求解下一个大于 x 或者下一个小于 x 这种题目了。比如上面的例子,我们就可以很容易地求出在其之后第一个小于其本身的位置。比如 3 的索引是 1,小于 3 的第一个索引是 4。也比如 2 的索引 4,小于 2 的第一个索引是 0,但是其在 2 的索引 4 之后,因此不符合条件,也就是不存在在 2 之后第一个小于 2 本身的位置。
上面的例子,我们在第 6 步开始 pop,第一个被 pop 出来的是 5,因此 5 之后的第一个小于 5 的索引就是 4。同理被 pop 出来的 3,4,5 也都是 4。
如果用 ans 来表示在其之后第一个小于其本身的位置,ans[i] 表示 arr[i] 之后第一个小于 arr[i] 的位置, ans[i] 为 -1 表示这样的位置不存在,比如前文提到的 2。那么此时的 ans 是 [-1,4,4,4,-1,-1,-1]。
第 8 步,我们又开始 pop 了。此时 pop 出来的是 9,因此 9 之后第一个小于 9 的索引就是 6。
这个算法的过程用一句话总结就是,如果压栈之后仍然可以保持单调性,那么直接压。否则先弹出栈的元素,直到压入之后可以保持单调性。 这个算法的原理用一句话总结就是,被弹出的元素都是大于当前元素的,并且由于栈是单调增的,因此在其之后小于其本身的最近的元素就是当前元素了
class Solution {
public int[] monostoneStack(int[] arr) {
int n = arr.length;
int[] answer = new int[n];
Deque<Integer> stack = new LinkedList();
for(int i = 0; i < n; i++) {
while (stack.size() != 0 && arr[stack.peek()] < arr[i]) {
int index = stack.pop();
answer[index] = i - index;
}
stack.push(i);
}
return answer;
}
}下面几个题帮助你理解单调栈, 并让你明白什么时候可以用单调栈进行算法优化。