单调栈定义:
从栈底元素到栈顶元素呈单调递增或单调递减,栈内序列满足单调性的栈。
一般用于求解,元素的左边或者右边第一个大于或者小于元素的值。
这里通过几道单调栈的例题进行举例分析。
单调递减栈:
场景:
- 在一个队列中针对每一个元素从它右边寻找第一个比它大的元素
- 在一个队列中针对每一个元素从它左边寻找第一个比它大的元素(从后往前遍历)
这里采用大神labuladong的算法小抄的两个例题举例:
例1.第一个更大的元素
题目:
给你⼀个数组,返回⼀个等⻓的数组,对应索引存储着下⼀个更⼤元素,如果没有更⼤的元素,就存-1。
例: 数组 [2,1,2,4,3],返回数组 [4,2,4,-1,-1]。
解释:第⼀个 2 后⾯⽐ 2 ⼤的数是 4; 1 后⾯⽐ 1 ⼤的数是 2;第⼆个 2 后⾯⽐ 2 ⼤的数是 4; 4 后⾯没有⽐ 4 ⼤的数,填 -1;3 后⾯没有⽐ 3 ⼤的数,填 -1。
题目很简单,就是找右边第一个比自己大的数,找到返回找到的数字,没有就返回-1.
这就是一个思路,从后往前遍历,栈中存放最大元素在栈底,
/*
给你⼀个数组,返回⼀个等
⻓的数组,对应索引存储着下⼀个更⼤元素,如果没有更⼤的元素,就存
-1。不好⽤语⾔解释清楚,直接上⼀个例⼦:
给你⼀个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。
解释:第⼀个 2 后⾯⽐ 2 ⼤的数是 4; 1 后⾯⽐ 1 ⼤的数是 2;第⼆个 2 后⾯
⽐ 2 ⼤的数是 4; 4 后⾯没有⽐ 4 ⼤的数,填 -1;3 后⾯没有⽐ 3 ⼤的数,填
-1。
*/
//单调递减栈
public static int[] convert(int[] param){
int[] result=new int[param.length];
Stack<Integer> stack=new Stack<>();
//倒着遍历
for (int i = param.length - 1; i >= 0; i--) {
//将最大的元素放入栈底
while(!stack.isEmpty()&&stack.peek()<param[i]){
stack.pop();
}
//while中pop出所有比param[i]小的元素,栈顶元素即是右边第一个可以满足的元素
result[i]=stack.isEmpty()?-1:stack.peek();
//放入,方便下一次比较
stack.push(param[i]);
}
return result;
}
例2.更温暖的气温
给你⼀个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近⼏天的天⽓⽓温(这⽓温是铁板烧?不是的,这⾥⽤的华⽒度)。你返回⼀个数组,计算:对于每⼀天,你还要⾄少等多少天才能等到⼀个更暖和的⽓温;如果等不到那⼀天,填 0 。
举例:给你 T = [73, 74, 75, 71, 69, 72, 76, 73],你返回 [1, 1, 4, 2, 1, 1, 0, 0]
和第一题类似,这里不做过多的解释,直接贴答案了
public static int[] convertT(int[] param){
int[] result=new int[param.length];
Stack<Integer> stack=new Stack<>();
for (int i = param.length - 1; i >= 0; i--) {
while(!stack.isEmpty()&¶m[stack.peek()]<param[i]){
stack.pop();
}
result[i]=stack.isEmpty()?0:stack.peek()-i;
stack.push(i);
}
return result;
}
例3.滑动窗口
题目
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
分析
该题也是一个递减栈的变形,我们只要同过stack存放一个递减栈,让每次取栈底元素就行了,这里使用双端队列,直接取就可以。
public int[] maxSlidingWindow(int[] nums, int k) {
int[] result = new int[nums.length - k + 1];
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < nums.length; i++) {
//单调递减栈组装数据
while(!stack.isEmpty()&&nums[stack.peekLast()]<nums[i]){
stack.pollLast();
}
stack.addLast(i);
//如果当前-栈顶元素大于个数限制,将栈顶元素出栈
if(i-stack.peekFirst()>=k){
stack.pollFirst();
}
//避免越界,当i-k+1>=0才有意义
if (i-k+1>=0){
result[i-k+1]=nums[stack.peekFirst()];
}
}
return result;
}
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sliding-window-maximum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
单调递增栈:
- 在一个队列中针对每一个元素从它右边寻找第一个比它小的元素
- 在一个队列中针对每一个元素从它左边寻找第一个比它小的元素(从后往前遍历)
例4.力扣真题
https://leetcode-cn.coml/problems/largest-rectangle-in-histogram/description/
穷举
其实可以想到的思路是穷举:
以2作为高,计算最大面积;
以1作为高,计算最大面积;
以5作为高,计算最大面积;
以6作为高,计算最大面积;
以2作为高,计算最大面积;
以3作为高,计算最大面积;
每个的结果取大,即最终结果
于是有了如下代码:
public class Leetcode84 {
public static void main(String[] args) {
Leetcode84 leetcode84 = new Leetcode84();
int[] param = {2,1,5,6,2,3};
System.out.println(leetcode84.largestRectangleArea(param));
}
public int largestRectangleArea(int[] heights) {
//遍历heights,当前元素作为构建形状的最低形状高度,由左和右分别判定可组成的长度。
int areaMax = 0;
for (int i = 0; i < heights.length; i++) {
int length = 1;
int leftIndex = i - 1;
int rightIndex = i + 1;
while (leftIndex >= 0 && heights[leftIndex] >= heights[i]) {
length++;
leftIndex--;
}
while (rightIndex < heights.length && heights[rightIndex] >= heights[i]) {
length++;
rightIndex++;
}
if (length * heights[i] > areaMax) {
areaMax = length * heights[i];
}
}
return areaMax;
}
}
力扣提交,结果:
....
单调栈
单调栈的处理逻辑
还是以题中的数据举例,原来数据如下。
为了更形象的使用这个逻辑,在头和尾补上元素0,
这里补上0主要是为了遍历避免判空,方便第一位和最后一位元素的判定。
转换后如下:
比如要找寻2组成的面积,那么就需要找寻左右两边第一个小于2的元素,通过栈,将0入栈,2入栈,当遇到1时,即可以求出左遍大于1的面积,求出后将其出栈。详细分析过程如下
详细过程分析
注意,我们这里举例的stack中存储的元素的value值一直是一个递增栈。
1. 源数据结构如下
2. 数组头和尾补0,注意这里的stack需要存储的是index值,而不是value值,因为stack用于帮我们找到宽度
3. 开始遍历数组,第一个元素为0
4. 继续遍历,处理index=1的元素
5. 遍历第三个元素,因为现在找到了元素的右边界,所以可以求出大于高度为1的元素的面积
6. 遍历第四个元素index=3
7. 遍历第五个元素index=4
8. 遍历第六个元素index=5,处理第五个元素,pop出index=4,计算高度为6的面积
9. 处理第四个元素,pop出index=3,处理高度为5的面积
10. 此时,由于第五个元素的值大于栈中arr[2]=1的值,此时index=5入栈
11.第七个元素入栈
12. 继续处理到第八个元素,发现不满足递增栈,即到了右边界,pop栈,计算高度为3的元素的值
13. 出栈后,由于0<arr[5],继续出栈,计算index为5的元素的面积
14. 由于0<arr[2],此时计算index=2的元素的面积
这样我们的所有元素的面积,就已经全部计算完毕。每个取大就可以了。
代码实现
package com.learn.mk.leetcode;
import java.util.Stack;
/**
* 84. 柱状图中最大的矩形
* 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
* <p>
* 求在该柱状图中,能够勾勒出来的矩形的最大面积。
* url:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/description/
*/
public class Leetcode84 {
public int largestRectangleArea(int[] heights) {
int tempMax = 0;
int[] heightsConvert = new int[heights.length + 2];
System.arraycopy(heights, 0, heightsConvert, 1, heightsConvert.length - 2);
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < heightsConvert.length; i++) {
while (!stack.isEmpty() && heightsConvert[stack.peek()] > heightsConvert[i]) {
Integer pop = stack.pop();
tempMax = Math.max(tempMax, heightsConvert[pop] * (i - stack.peek()-1));
}
stack.push(i);
}
return tempMax;
}
public static void main(String[] args) {
Leetcode84 leetcode84 = new Leetcode84();
int[] param = {2,1,5,6,2,3};
System.out.println(leetcode84.largestRectangleArea(param));
}
}
可以进一步优化代码,减少判定逻辑,如下:
package com.learn.mk.leetcode;
import java.util.Stack;
/**
* 84. 柱状图中最大的矩形
* 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
* <p>
* 求在该柱状图中,能够勾勒出来的矩形的最大面积。
* url:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/description/
*/
public class Leetcode84 {
public int largestRectangleArea(int[] heights) {
int tempMax = 0;
int[] heightsConvert = new int[heights.length + 2];
System.arraycopy(heights, 0, heightsConvert, 1, heightsConvert.length - 2);
Stack<Integer> stack = new Stack<>();
stack.push(0);
for (int i = 0; i < heightsConvert.length; i++) {
while (heightsConvert[stack.peek()] > heightsConvert[i]) {
Integer pop = stack.pop();
tempMax = Math.max(tempMax, heightsConvert[pop] * (i - stack.peek()-1));
}
stack.push(i);
}
return tempMax;
}
public static void main(String[] args) {
Leetcode84 leetcode84 = new Leetcode84();
int[] param = {2,1,5,6,2,3};
System.out.println(leetcode84.largestRectangleArea(param));
}
}