前言
记录 LeetCode 平台刷题时遇到的单调栈相关题目
739.每日温度
单调栈使用场景:
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。如果一开始判断不出单调性要为单调递增还是单调递减可以两种都思考一下,如果使用递增的单调性要如何操作,使用递减的单调性又如何操作
保持栈中 从栈底到栈顶,元素值的大小是 递减 的;
由于本题需要计算元素右边第一个比自己大的元素与自己的距离,所以栈中的元素用元素在 temperatures 数组中对应的下标,可以直接根据下标差值计算距离;而且数组元素下标跟结果数组的元素和下标是一一对应的,如果栈中存数组元素值的话还要去找到它在数组中对应的下标,才能计算距离
使用 res 数组记录答案。遍历一次数组,会有三种情况:
- 当遍历到的元素值 temperature[i] 大于栈顶下标对应的数组元素值 temperature[topEle],说明下标 topEle 的数组元素右边第一个大于它的数组元素就是 temperature[i],所以 res[topEle] = i - topEle
- 当遍历到的元素值 temperature[i] 小于栈顶下标对应的数组元素值,符合单调栈要求的顺序,直接入栈
- 当遍历到的元素值 temperature[i] 等于栈顶下标对应的数组元素值,由于我们只需要找到元素右边第一个大于它的元素,所以等于并不影响,直接入栈
public int[] dailyTemperatures(int[] temperatures) {
LinkedList<Integer> stack = new LinkedList<>();
int length = temperatures.length;
int res[] = new int[length];
//注意peek(),poll()时对空指针的判断,判断栈中是否有元素
//借助单调栈,时间复杂度为 O(n)
for(int i = 0;i < length;i++){
int topEle;
while(stack.size() > 0 && temperatures[i] > temperatures[(topEle = stack.peek())]){
res[topEle] = i - topEle;
stack.poll();
}
stack.push(i);
}
return res;
}
496.下一个最大元素Ⅰ- 单调栈 + 哈希表
如何使时间复杂度为 O(n + m)
在 nums2 中找下一个最大元素的算法跟 739 题基本一样,这里复杂度为 O(n)
麻烦的地方就在于如何将 nums1 中的元素跟 nums2 的元素对上号,暴力做法的话时间复杂度为 O(n*m)。由于所有元素都是不重合的,所以可以用一个哈希表记录,键为 nums1 中的元素值,值为元素在 nums1 中的下标
遍历 nums2 时,每找到一个元素的下一个最大元素时就判断这个元素在哈希表中存不存在,存在的话就将找到的下一个最大元素放入结果数组。
还有一个问题是,如果 nums1 中的元素在 nums2 中找不到下一个最大元素时,它对应的结果应该是 -1,而 739 题中找不到的话对应的结果是 0,所以这里还需要对结果数组先初始化所有元素值为 -1
将初始化结果数组和生成哈希表放在一个循环,加上一个遍历 nums2 找下一个最大元素的循环,整体时间复杂度就是 O(n + m)
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
HashMap<Integer,Integer> map = new HashMap<>();
int res[] = new int[len1];
for(int i = 0;i < len1;i++){
res[i] = -1;
map.put(nums1[i],i);
}
LinkedList<Integer> stack = new LinkedList<>();
for(int i = 0;i < len2;i++){
int topEle;
while(stack.size() > 0 && nums2[i] > (topEle = stack.peek())){
Integer j = map.getOrDefault(topEle,null);
if(j != null) res[j] = nums2[i];
stack.poll();
}
stack.push(nums2[i]);
}
return res;
}
503. 下一个更大元素Ⅱ
本题关键操作为每次遍历数组时要遍历两篇
public int[] nextGreaterElements(int[] nums) {
int l = nums.length;
int res[] = new int[l];
Arrays.fill(res,-1);
LinkedList<Integer> stack = new LinkedList<>();
//对每一个元素都要向右循环遍历一次数组才可以,直接遍历每一个元素时再套一层循环遍历剩下整个数组的话复杂度为O(n*n)
//直接遍历两遍数组,可以满足对于每一个元素,数组中剩下的所有元素都有被遍历到。复杂度为O(2 * n)
int topEle;
for(int i = 0;i < 2 * l;i++){
while(stack.size() > 0 && nums[i % l] > nums[(topEle = stack.peek())]){
res[topEle] = nums[i % l];
stack.poll();
}
stack.push(i % l);
}
return res;
}
84.柱状图中最大的矩形
一开始我的想法是,想用动态规划。对于从 0 到第 i 个矩形,当加入第 i + 1 个矩形时,寻找第 i + 1 个矩形与前面 0 到 i 的矩形能勾勒出的所有矩形,这就是从左至右当加入第 i + 1 个矩形时会出现的新情况,同时会维护前面从 0 到 i 找到的最大矩形,拿这个记录与加入第 i + 1 个矩形带来的新的可能矩形比较,得到从 0 到 i + 1,最大的矩形面积。
思想应该没有问题,但可能出现的矩形长度情况复杂,而一个能勾勒出的矩形又可能与许多连续的矩形有关,“寻找第 i + 1 个矩形与前面0到i的矩形能勾勒出的所有矩形”,这一步就很复杂,很困难
看了题解的想法是,对于第 i 个矩形,分别向左向右找到左边第一个长度小于 heights[i] 的矩形的下标 left 和右边第一个长度小于 heights[i] 的矩形下标 right,然后对于第i个矩形来说,height[i] * (right - left - 1) 就是它带来的能勾勒出的最大面积矩形
相当于 left 和 right 两个中间夹着的区域能勾勒出的最大矩形,也就是说第 i 个矩形带来的能勾勒出的最大面积的矩形的高肯定就是这个第 i 个矩形的高,这里说的 “第 i 个矩形带来的能勾勒出的最大面积”,不是说找出来的这个以它的长度为高的矩形就是有包含它的最大矩形,而是针对于这个矩形,我们只需要去找到以它的长度为高的最大矩形就行,没必要去找有它参与的每一个矩形,再找出最大的那个。这么一看果然是我又把问题复杂化了
根据上面提到的解题想法 (加粗那段话),用单调栈解题。单调栈里的元素是矩形元素的下标。令单调栈从栈底到栈头是递增顺序,当当前要放入栈的矩形长度小于栈顶矩形长度时,栈顶在栈中的下一个元素对应的矩形的长度也是小于栈顶矩形长度的,此时就可以求出针对栈顶元素,我们要计算的最大矩形面积,这个栈顶元素计算过后就可以弹出了,然后再循环对新的栈顶元素进行判断,直到当前要放入栈的矩形长度大于栈顶元素对应的矩形的长度
大致思路就是上述这样,不过会遇到几个 边界情况:
- 如果矩形数组本来就是递增序列,那么所有矩形(下标)都会直接入栈,也就不会出现计算最大矩形的过程,直接返回maxI的初始值0(maxI是代码中用于记录能勾勒出的最大矩形的面积)。一开始我的解决方法很直接地想到的就是在方法最后判断maxI是不是还是初始值0,是的话就用数组中第一个矩形的长度乘以数组的长度得到答案,因为第一个矩形肯定就是长度最小的矩形,最大勾勒矩形的高也就是这个长度
- 然后第二种边界就出来了:如果矩形数组是先一段前导0,再加一段长度递增的矩阵序列,那就不能用第一个矩形的长度作为高计算最大矩形的面积了。这里我一开始的解决方法就是在一开始先去掉先导0,重置数组的起点
- 这么做的话,第三种wa(wrong answer)的情况又出来了:矩阵数组有递增也有递减,但最后的一段是递增的,这样的话,最后的这一段还是会像第1种边界说的那样,到最后全部都存入了栈,没有被处理,比如[2,1,2]这个样例,到最后栈中会剩下1跟2,此时maxI的值是由第一个矩形2带来的2,后面没有其他操作,直接返回了2,显然3才是正确答案。想到这有点想法了,那就是必须让每一个矩形都得经过计算针对于它的最大矩形这一步,怎么做到呢?就是在矩形数组后加一个后导0
所以最后能 AC 的代码如下:
public int largestRectangleArea(int[] heights) {
int i = 0;
int l = heights.length;
//加一个后导0,避免最后出现一段递增的矩阵序列,然后都被存在栈里而没有被取出来计算矩形
int newHeight[] = new int[l + 1];
for(int j = 0;j < l;j++){
newHeight[j] = heights[j];
}
newHeight[l] = 0;
l++;
//排除前导0,避免前导0后面的矩形是递增序列,
while(i < l && newHeight[i] == 0) i++;
if(i == l) return 0;
//设置排除前导0后新的数组起点坐标
int newZero = i;
LinkedList<Integer> stack = new LinkedList<>();
int maxI = 0;
for(;i < l;i++){
int topEle;
while(stack.size() > 0 && newHeight[(topEle = stack.peek())] > newHeight[i]){
int t = stack.poll();
int j = stack.size() > 0 ? stack.peek() : newZero - 1;
maxI = Math.max(maxI,newHeight[t] * (i - j - 1));
}
stack.push(i);
}
//如果maxI还是0,没有被修改,说明矩阵列就是递增列,那么最大矩形的高就是数组第一个矩形的高(注意是新的数组起点)
return maxI == 0? newHeight[newZero] * (l - newZero) : maxI;
}
不过再仔细想想,既然加了后导 0,前面说到的第一和第二种情况都可以解决了,所以把多余的步骤清掉,只需加一个后导 0 即可。得到下一版代码:
public int largestRectangleArea(int[] heights) {
int i = 0;
int l = heights.length; int newHeight[] = new int[l + 1];
for(int j = 0;j < l;j++){
newHeight[j] = heights[j];
}
newHeight[l] = 0;
l++;
LinkedList<Integer> stack = new LinkedList<>();
int maxI = 0;
for(;i < l;i++){
int topEle;
while(stack.size() > 0 && newHeight[(topEle = stack.peek())] > newHeight[i]){
int t = stack.poll();
int j = stack.size() > 0 ? stack.peek() : - 1;
maxI = Math.max(maxI,newHeight[t] * (i - j - 1));
}
stack.push(i);
}
return maxI;
}
所以说,加后导 0 这一步,很灵活很有用
42.接雨水
首先要明确:雨水的计算可以按行来算:
任意两个中间存在凹槽的柱子就可以确定中间每一行的雨水量
也可以按列来算:
如果想按列来算的话,某一列上的雨水量就是它左边最高的柱子与右边最高的柱子两者中较小的高度乘以它的宽度,也就是 1,所以可以用双指针来实现这个思路
不过我想用单调栈完成这道题,因为雨水总是产生在两根高长度的柱子中间夹着的低高度柱子区域,所以单调栈的单调顺序应该是从栈底到栈头递减的顺序,当当前要入栈的柱子的高度大于栈顶柱子的高度时,就可以开始计算雨水量,计算时需要知道柱子间的距离,所以栈中的元素存的是下标
思考一下此时是要按行计算还是按列计算?单调栈每次遇到的都是凹槽所在列往左第一个和往右第一个高于凹槽列柱子长度的柱子,借助这两个柱子算出来的中间凹槽列的雨水量不一定是这一列的最终拥有的雨水量,因为在这两个柱子左边右边可能还有更高的柱子,所以要使用单调栈的话就不好按列来算,适合按行来算
public int trap(int[] height) {
int l = height.length;
LinkedList<Integer> stack = new LinkedList<>();
int maxRain = 0;
for(int i = 0;i < l;i++){
// 当遇到比栈顶柱子大的柱子时,可以计算该柱子与栈顶柱子左边高度小于等于该柱子的柱子之间各行的雨水量
while(stack.size() > 0 && height[i] > height[stack.peek()]){
int topEle = stack.poll();
// 如果栈顶柱子左边没有其它柱子,那就计算不了雨水量了
if(stack.size() > 0){
int h = Math.min(height[i],height[stack.peek()]) - height[topEle];
maxRain += h * (i - stack.peek() - 1);
}
}
stack.push(i);
}
return maxRain;
}
做了 84 题跟 42 题,感觉自己进入了一个误区,就是在思考算法时,总想每一次操作都能兼顾到所有的情况,但其实很多不同步的操作之间是存在重叠的。比如84题中,一心想着每处理一个矩形时,要找出所有有它参与构成的矩形;42题中,每处理一个凹槽时,就想一次性算出整个凹槽的雨水量
402.移掉K位数字
要想最后得到的结果越小,我们知道越往前的数字越大,那么它整个数就越大,所以解题思路就是从左到右遍历数字,如果遇到比前面遍历到的数更小的,就把前面遍历到的数删掉,替换成刚刚遇到的这个数
这个思路就跟单调栈很像了:维护一个从栈底到栈顶单调递增的单调栈,最后按照从栈底到栈顶的顺序取出每个数字组成的就是结果。当然最多只能删掉 k 个数
public String removeKdigits(String num, int k) {
int len = num.length();
LinkedList<Character> stack = new LinkedList<>();
//建立单调栈
for(int i = 0;i < len;i++){
char c = num.charAt(i);
//最多只能删k个
while(!stack.isEmpty() && k > 0 && stack.peekLast() > c){
stack.removeLast();
k--;
}
stack.addLast(c);
}
/*优化:将这段代码注释掉,然后将下面的原来是int size = stack.size()
改成了int size = stack.size() - k,减去了这一步,但结果还是一样的
for(int i = 0;i < k;i++){
stack.removeLast();
}*/
StringBuilder sb = new StringBuilder();
//有可能留下的都是0,所以在把栈中的数字拼凑回一个整数时,要记录数字是不是都是0 (如果是的话,在这里最后会得到res是一个空串
boolean formerZero = true;
//前面可能删不够k个,此时只能选择从左到右前size位数
int size = stack.size() - k;
//循环过程stack的size会变化,不能用i < stack.size()作为条件
for(int i = 0;i < size;i++){
char c = stack.removeFirst();
if(formerZero && c == '0'){
continue;
}
sb.append(c);
formerZero = false;
}
String res = sb.toString();
//可能最后删成一个空串,应该返回0
return res.equals("") ? "0" : res;
}
456. 132 模式
从后往前遍历数组,同时维护一个从栈底到栈顶单调递减的单调栈,用变量 k 维护在维护单调栈的过程中被弹出栈的所有元素中的最大值
那么,一旦遍历到出现 nums[i] < k,根据单调栈的性质,有 k < 栈顶元素,且三者的下标关系满足 i < 栈顶元素的下标 < k 值元素的下标,所以符合 132 模式的定义,返回 true
public boolean find132pattern(int[] nums) {
int len = nums.length;
LinkedList<Integer> mStack = new LinkedList<>();
int k = Integer.MIN_VALUE;
for (int i = len - 1; i >= 0; i--) {
if (nums[i] < k) return true;
while (!mStack.isEmpty() && mStack.peekFirst() < nums[i]) {
//由于是单调递减的栈,所以越后面被弹出的元素值也会越大,无需k = Math.max(k,mStack.pollFirst())
k = mStack.pollFirst();
}
mStack.addFirst(nums[i]);
}
return false;
}