栈7:单调栈的原理和应用
Posted 纵横千里,捭阖四方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了栈7:单调栈的原理和应用相关的知识,希望对你有一定的参考价值。
在刷LeetCode之前 ,我看到过单调栈,感觉就是在栈的基础上加了个要求而已,没怎么当回事。但是在刷LeetCode题目的时候,才知道有些题用单调栈非常好使。本着学会一个,刷掉一片的原则,我们用几篇来研究一下这个结构,以及相关的题目。
1.单调栈的概念
单调栈是前一节介绍的最大最小栈的内涵是相通的,单调栈就是栈里面存放的数据都是有序的,所以可以分为单调递增栈和单调递减栈两种。
单调递增栈就是从栈底到栈顶是从大到小
单调递减栈就是从栈底到栈顶是从小到大
为什么要使用单调栈?
why搞出一个单调栈的东西?哲学认为凡是存在即合理,而算法里之所以搞出这么个东西,一定是在解决某些问题的时候特别好用,然后就给起了个名字(如果这样那是不是会有个单调队列呢?有的!优先级队列不就算吗?)。
言归正传,就拿LeetCode503. 下一个更大元素 II 这道题来讲,如果利用常规解法,对于每个数而言,我们需要遍历其右边的数,直到找到比自身大的数,这是一个 O(n^2) 的做法。之所以是 O(n^2),是因为每次找下一个最大值,我们是通过遍历来实现的。如果面试的时候遇到这个问题了你采用常规解法可能你就gg了。而使用单调栈的话你就可以将复杂度降到O(n),这就是你为啥要使用单调栈的原因了。
2.什么时候使用单调栈?
问题又来了啥时候使用呢?一般来讲,【找左右两边比你大或者比你小】的问题,都可以使用单调栈来解决。所以遇到这种问题一定要记得考虑一下单调栈能不能解决问题。
单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录左(右)边第一个比当前元素大(小)的元素,优点是只需要遍历一次。
单调栈里存放的元素是什么?
单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接用 nums[i] 就可以获取
下面我们看几个例子:
3.LeetCode496:下一个更大元素I
先看题目:
给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
例子1:
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
例子2:
输入: nums1 = [2,4], nums2 = [1,2,3,4].
输出: [3,-1]
解释:
对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。
对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
首先,该题使用暴破的时间复杂度(n*m)偏高,所以为了降低时间复杂度,我们采用另外一种方式:单调栈。这个题目有点长,其实意思很简单,就是先对第二个按照从小到大排一下顺序,为了便于查找时操作,我们用栈来存储,同时还使用了Hash来保存对应关系。
你可能会有疑问,该方法好在哪里呢?其实该方法采用先行将 nums2中的数字,对应的下一个更大的数字已经找出来,然后放到哈希表中,以供后面 nums1直接使用即可,我们这样做,可以将时间复杂度降到 n + mn+m。
具体流程如下:
-
创建一个临时栈,一个哈希表,然后遍历 nums2。
-
若当前栈无数据,则当前数字入栈备用。
-
若当前栈有数据,则用当前数字与栈顶比较:
当前数字 > 栈顶,代表栈顶对应下一个更大的数字就是当前数字,则将该组数字对应关系,记录到哈希表。
当前数字 < 栈顶,当前数字压入栈,供后续数字判断使用。
4.这样,我们就可以看到哈希表中存在部分 nums2 数字的对应关系了,而栈中留下的数字,代表无下一个更大的数字,我们全部赋值为 −1 ,然后存入哈希表即可。
5.遍历 nums1,直接询问哈希表拿对应关系即可。
public class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
Deque<Integer> stack = new ArrayDeque<>();
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < len2; i++) {
while (!stack.isEmpty() && stack.peekLast() < nums2[i]) {
map.put(stack.removeLast(), nums2[i]);
}
stack.addLast(nums2[i]);
}
int[] res = new int[len1];
for (int i = 0; i < len1; i++) {
res[i] = map.getOrDefault(nums1[i], -1);
}
return res;
}
}
4.举例 LeetCode 503 下一个更大元素II
这个题也是对上面题目的条件做了改动,先看题目要求:
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
示例 1:
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
我们可以使用单调栈解决本题。单调栈中保存的是下标,从栈底到栈顶的下标在数组 nums 中对应的值是单调不升的。
每次我们移动到数组中的一个新的位置 i,我们就将当前单调栈中所有对应值小于nums[i] 的下标弹出单调栈,这些值的下一个更大元素即为nums[i](证明很简单:如果有更靠前的更大元素,那么这些位置将被提前弹出栈)。随后我们将位置 i入栈。
但是注意到只遍历一次序列是不够的,例如序列 [2,3,1],最后单调栈中将剩余 [3,1],其中元素 [1] 的下一个更大元素还是不知道的。我们要找每一个元素的下一个更大的值,因此我们需要对原数组遍历两次,对遍历下标进行取余转换。
建立「单调递减栈」,并对原数组遍历一次:
-
如果栈为空,则把当前元素放入栈内;
-
如果栈不为空,则需要判断当前元素和栈顶元素的大小:
1.如果当前元素比栈顶元素大:说明当前元素是前面一些元素的「下一个
更大元素」,则逐个弹出栈顶元素,直到当前元素比栈顶元素小为止。
2.如果当前元素比栈顶元素小:说明当前元素的「下一个更大元素」与
栈顶元素相同,则把当前元素入栈。
上代码:
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] ret = new int[n];
Arrays.fill(ret, -1);
Deque<Integer> stack = new LinkedList<Integer>();
for (int i = 0; i < n * 2 ; i++) {
while (!stack.isEmpty() && nums[stack.peek()] < nums[i % n]) {
ret[stack.pop()] = nums[i % n];
}
stack.push(i % n);
}
return ret;
}
可能刚开始有点看不懂没关系可以尝试一下测试版:
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] ret = new int[n];
Arrays.fill(ret, -1);
Deque<Integer> stack = new LinkedList<Integer>();
for (int i = 0; i < n * 2 - 1; i++) {
System.out.println("第"+i+"轮循环");
while (!stack.isEmpty() && nums[stack.peek()] < nums[i % n]) {
System.out.println("数字"+nums[stack.peek()]+"小于"+"数字"+nums[i % n]);
System.out.println("让ret"+stack.peek()+"的值为"+nums[i % n]+",弹出"+stack.peek());
ret[stack.pop()] = nums[i % n];
}
stack.push(i % n);
System.out.println("压入元素"+stack.peek());
System.out.println(" ");
}
return ret;
}
以上是关于栈7:单调栈的原理和应用的主要内容,如果未能解决你的问题,请参考以下文章