LeetCode 16 - 单调栈
单调栈还是一个栈,只是要求每次入栈的元素必须有序——如果新元素入栈时不符合要求,就将之前的元素出栈,直到符合要求才入栈,形成一个 单调递增/减 的栈。(递增递减是根据出栈序列来说的,即从栈顶到栈底)
- 单调递增:只有比栈顶小的才能入栈,适用于求解 第一个大于该位置元素的数。
- 单调递减:只有比栈顶大的才能入栈,适用于求解 第一个小于该位置元素的数。
代码模板:
Deque<Integer> stack = new LinkedList<>();
for(int i = 0; i < nums.length; i++) {
// 递减栈
while(!stack.isEmpty() && stack.peek() > nums[i])
stack.pop();
stack.push(nums[i]);
}
84. 柱状图中最大的矩形#
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够 勾勒 出来的矩形的最大面积。
可以考虑枚举矩形的宽和高:
-
如果 枚举宽,可以使用两重循环枚举矩形的 左右边界 以确定这个宽,此时矩形的高度是在左右边界之内的柱子的 最小高度。
int largestRectangleArea(int[] heights) { int len = heights.length; int area = 0; // 枚举左边界 for(int left = 0; left < len; left++) { int minHeight = heights[left]; // 枚举右边界 for(int right = left; right < n; right++) { minHeight = Math.min(minHeight, heights[right]); area = Math.max(area, (right-left+1) * minHeight); } } return area; }
-
如果 枚举高,可以使用一层循环枚举某一根柱子,将其固定为矩形的高 h,然后从这根柱子开始 向两侧延伸,直到 遇到高度小于 h 的柱子,就确定了矩形的左右边界。
int largestReactangleArea(int[] heights) { int len = heights.length; int area = 0; // 枚举每一根柱子 for(int mid = 0; mid < n; mid++) { int height = heights[mid]; // 向两边延伸寻找左右边界 int left = mid, right = mid; // 注意这里的 left-1, right+1 while(left > 0 && heights[left-1] >= height) left--; while(right < n && heights[right+1] >= height) right++; // 此时 left, right 即为左右边界 area = Math.max(area, (right-left+1) * height); } return area; }
这两种暴力方法的时间复杂度都为 ,需要进行优化,考虑到枚举高的方法使用了一层循环,我们来优化这种方法。
方法:单调栈
思路:对每一个高度,可以求得左右边界,继而求出对应的面积;那么遍历所有高度,即可求出最大面积。使用单调栈,在出栈操作时得到前后边界并计算面积。
- 栈中存放了
j
值,即某些柱子对应的 索引,它们对应的高度值单调递增。 - 枚举到第
i
根柱子时,从栈顶不断移除所有对应高度值大于等于当前高度heights[i]
的索引。移除完毕后,栈顶的索引值j
就是i
左侧距离最近的小于其高度的柱子。- 这里有一种 特殊情况,如果移除后栈变为空,说明左侧所有柱子都不低于当前柱子,那么可以认为这个
j
值为-1
。(这个虚拟的柱子称为 哨兵)
- 这里有一种 特殊情况,如果移除后栈变为空,说明左侧所有柱子都不低于当前柱子,那么可以认为这个
- 移除完成后,将当前柱子的索引
i
放入栈顶。
上面的步骤可以求出各个柱子的左边界,用相同的方法,但是 从右向左遍历,就可以得到各个柱子的 右边界。
int largestRectangleArea(int[] heights) {
int len = heights.length;
int[] left = new int[len];
int[] right = new int[len];
Deque<Integer> stack = new LinkedList<>();
// 填充 left 数组(各个柱子的左边界)
for (int i = 0; i < len; i++) {
// 找到栈中第一个小于当前高度的柱子
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) stack.pop();
// left[i] 表示柱子 i 的高度确定的左边界
left[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i); // 将当前索引入栈
}
// 填充右边界数组
stack.clear();
for (int i = n - 1; i >= 0; i--) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) stack.pop();
right[i] = stack.isEmpty() ? n : stack.peek();
stack.push(i);
}
int area = 0;
for (int i = 0; i < n; i++)
area = Math.max(area, (right[i] - left[i] - 1) * heights[i]);
return area;
}
优化:上面的方法在确定左右边界需要分别进行一次遍历,其实可以进行优化,只使用一次遍历就可以确定两个边界。因为 在对索引 i
进行出栈时,可以确定它的右边界。当索引 i
出栈时,说明当前正在处理的索引 i0
对应柱子的高度 小于等于 i
的高度,所以索引 i0
就是柱子 i
的右边界。
int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
Arrays.fill(right, n); // 注意对 right 的初始化
Deque<Integer> stack = new LinkedList<>();
for(int i = 0; i < n; i++) {
// 确定左边界的方法不变
while(!stack.isEmpty()
&& heights[stack.peek()] >= heights[i]) {
right[stack.peek()] = i; // 在出栈时确定栈顶元素右边界
stack.pop();
}
left[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
int area = 0;
for(int i = 0; i < n; i++)
area = Math.max(area, (right[i]-left[i]-1) * heights[i]);
return area;
}
85. 最大矩形#
给定一个仅包含 0
和 1
、大小为 rows x cols
的二维二进制矩阵,找出只包含 1
的最大矩形,并返回其面积。
方法一:优化暴力解法
首先计算每个元素的 左边(包括自身)连续 1 的数量,使用 left[i][j]
记录。
随后,我们枚举 以该点为右下角的全 1 矩形:
- 首先看当前行,因为高度为 1,所以全 1 矩形面积为
left[i][j] * 1
; - 然后向上扩展一行,矩形高度变成 2,宽度变成了
min(left[i][j], left[i-1][j])
; - 以此类推,直到扩展到第一行。
对每个点重复这个过程,就能得到所有矩形里面的最大矩形。
我们 预计算最大宽度 的方法实际上将输入转换成了一系列柱状图,针对柱状图计算最大面积。
int maxRectangle(char[][] matrix) {
int rows = matrix.length;
if(rows == 0) return 0;
int cols = matrix[0].length;
// 计算 left 数组
int[][] left = new int[rows][cols];
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
if(matrix[i][j] == '1')
left[i][j] = 1 + (j == 0 ? 0 : left[i][j-1]);
}
}
int result = 0;
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
if(matrix[i][j] == '0') continue;
int width = left[i][j];
int area = width;
for(int k = i-1; k >= 0; k--) {
width = Math.min(width, left[k][j]);
area = Math.max(area, (i-k+1) * width);
}
result = Math.max(result, area);
}
}
return result;
}
方法二:单调栈
在方法一中,将输入拆分成了 一系列柱状图,为了计算最大矩形,只需要计算 每个柱状图 中的最大面积,并找到全局最大值即可。
和 84 题实际上相同,只是柱状图旋转了 90 度,所以原来的为每一根柱子寻找左右边界,变成了为每一根(横着放置)的柱子寻找上下边界。
int maxRectangle(char[][] matrix) {
int rows = matrix.length, cols = matrix[0].length;
// 填充 left 数组(构成一系列柱状图)
int[][] left = new int[rows][cols];
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
if(matrix[i][j] == '1')
left[i][j] = 1 + (j == 0 ? 0 : left[i][j-1]);
}
}
int maxArea = 0;
// 枚举每一列,计算以该列为底线的柱状图
for(int j = 0; j < cols; j++) {
// 因为柱状图旋转了,所以左右边界变成了上下边界
int[] top = new int[rows];
int[] bottom = new int[rows];
Deque<Integer> stack = new LinkedList<>();
// 从第一行开始枚举柱子的「高」,寻找上边界
for(int i = 0; i < rows; i++) {
while(!stack.isEmpty()
&& left[stack.peek()][j] >= left[i][j])
stack.pop();
top[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
// 从最后一行开始枚举柱子的高,寻找下边界
stack.clear();
for(int i = rows-1; i >= 0; i--) {
while(!stack.isEmpty()
&& left[stack.peek()][j] >= left[i][j])
stack.pop();
bottom[i] = stack.isEmpty() ? rows : stack.peek();
stack.push(i);
}
for(int i = 0; i < rows; i++) {
int height = bottom[i] - top[i] - 1;
int area = height * left[i][j];
maxArea = Math.max(area, maxArea);
}
}
return maxArea;
}
仿照 84 题,也可以将这里的填充 top, bottom 数组操作利用一次循环完成:
int[] top = new int[rows];
int[] bottom = new int[rows];
Arrays.fill(bottom, rows);
for(int i = 0; i < rows; i++) {
while(!stack.isEmpty()
&& left[stack.peek()][j] >= left[i][j]) {
bottom[stack.peek()] = i;
stack.pop();
}
top[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
503. 下一个更大元素 II#
给定一个循环数组 nums
( nums[nums.length - 1]
的下一个元素是 nums[0]
),返回 nums
中每个元素的 下一个更大元素 。
数字 x 的 下一个更大的元素是 按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。
方法:单调栈
递增递减的定义是根据从 栈顶 → 栈底 的顺序来说的:
- 单调 递增栈 中 最小元素位于栈顶,用于求解 第一个大于当前位置元素的数;(假设当前位置为
i
,那么栈顶元素如果小于nums[i]
,那么会被弹出,弹出的同时 它们也找到了 下一个更大元素,就是i
处的元素) - 单调 递减栈 中 最大元素位于栈顶,用于求解 第一个小于当前位置元素的数。
本问题要求出下一个更大元素,可以利用 递增栈。每枚举到一个元素位置 i
,首先将栈中所有小于等于 nums[i]
的元素出栈,这些 被弹出元素的下一个更大的元素 就是 nums[i]
。然后将 i
入栈。
但因为是循环数组,所以只遍历一遍还不行,例如 [2, 3, 1]
遍历完成后栈中元素为 [3, 1]
,1 的下一个更大元素还不知道。这可以通过 扩展下标遍历范围 为 2*n
并 对下标取模 来处理。
int nextGreaterElement(int[] nums) {
int len = nums.length;
int[] result = new int[len];
Arrays.fill(result, -1); // 如果没有更大的就记为 -1
Deque<Integer> stack = new LinkedList<>();
for(int i = 0; i < 2 * len; i++) {
while(!stack.isEmpty() && nums[stack.peek()] < nums[i % len]) {
// 此时栈顶元素的下一个更大元素就是 nums[i]
result[stack.pop()] = nums[i % len];
}
// 将索引入栈
stack.push(i % len);
}
return result;
}
739. 每日温度#
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指在第 i
天之后,才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
方法:单调栈
这个问题和 503 题(求解下一个更大元素)很相似,但是更简单;不同之处在于,要求解的不是下一个更大元素,而是下一个更大元素与当前元素的距离。
public int[] dailyTemperatures(int[] temperatures) {
int len = temperatures.length;
int[] result = new int[len];
Arrays.fill(result, 0);
Deque<Integer> stack = new LinkedList<>();
for(int i = 0; i < len; i++) {
while(!stack.isEmpty()
&& temperatures[stack.peek()] < temperatures[i]) {
result[stack.peek()] = i - stack.peek();
stack.pop();
}
stack.push(i);
}
return result;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义