前置知识(单调栈)
单调栈是一种特殊的栈结构,它在解决一类特定的问题时非常有效,特别是那些涉及序列中元素之间相对大小关系的问题。单调栈内的元素按照单调递增或单调递减的顺序排列。使用单调栈可以帮助我们快速找到一个元素左边或右边第一个比它大(或小)的元素。
单调递增栈
单调递增栈保证栈内元素从栈底到栈顶递增。当一个新元素要入栈时,如果这个新元素大于栈顶元素,它就直接入栈;如果小于等于栈顶元素,则将栈顶元素弹出,直到栈顶元素小于新元素,然后新元素入栈。这种方式保证了栈内的元素单调递增。
应用场景:适用于解决问题如“找到数组中每个元素右侧第一个比它大的元素”。
单调递减栈
单调递减栈保证栈内元素从栈底到栈顶递减。当一个新元素要入栈时,如果这个新元素小于栈顶元素,它就直接入栈;如果大于等于栈顶元素,则将栈顶元素弹出,直到栈顶元素大于新元素,然后新元素入栈。这种方式保证了栈内的元素单调递减。
应用场景:适用于解决问题如“找到数组中每个元素右侧第一个比它小的元素”。
示例
假设有一个数组 [2, 1, 4, 6, 5]
,我们要找出数组中每个元素右侧第一个比它大的元素。
- 使用单调递减栈。初始时,栈为空。
- 遍历数组,元素
2
入栈。 - 元素
1
入栈,因为1 < 2
,栈内元素单调递减。 - 元素
4
不小于栈顶元素1
,弹出1
,此时4
是1
右侧第一个比它大的元素。继续比较4
和下一个栈顶元素2
,4
也大于2
,弹出2
,4
是2
右侧第一个比它大的元素。然后4
入栈。 - 元素
6
入栈前,先弹出所有小于6
的元素,即弹出4
。6
是4
右侧第一个比它大的元素。然后6
入栈。 - 元素
5
入栈,因为5 < 6
,直接入栈。
通过上述步骤,我们可以找到数组中每个元素右侧第一个比它大的元素。对于数组 [2, 1, 4, 6, 5]
,其结果是 [4, 4, 6, -1, -1]
(这里 -1
表示没有找到符合条件的元素)。
实现代码
下面是一个使用单调栈的基本实现代码示例,用于解决“找到数组中每个元素右侧第一个比它大的元素”的问题:
import java.util.Arrays;
import java.util.Stack;
public class NextGreaterElement {
public static void main(String[] args) {
int[] nums = {2, 1, 4, 6, 5};
int[] result = nextGreaterElement(nums);
System.out.println(Arrays.toString(result)); // 打印结果
}
public static int[] nextGreaterElement(int[] nums) {
int[] ans = new int[nums.length]; // 存储结果
Stack<Integer> stack = new Stack<>(); // 单调栈,存储元素索引
Arrays.fill(ans, -1); // 默认值为-1,表示没有找到符合条件的元素
for (int i = 0; i < nums.length; i++) {
// 当前元素大于栈顶元素时,弹出栈顶元素,并更新结果数组
while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
ans[stack.pop()] = nums[i];
}
stack.push(i); // 当前元素索引入栈
}
return ans;
}
}
这段代码通过维护一个单调递减的栈来找到数组中每个元素右侧第一个比它大的元素,从而高效解决了这类问题。
题目
题解
计算直方图中最大的矩形面积。这个问题要求我们在一个由非负整数表示的高度数组heights
中找到能够形成的最大矩形的面积。代码使用了单调栈的数据结构来高效地解决这个问题。下面是代码的详细解读和注释添加:
class Solution {
public int largestRectangleArea(int[] heights) {
// left 和 right 数组用于存储每个柱子左侧和右侧第一个小于当前柱子高度的柱子的索引
int[] left = new int[heights.length];
int[] right = new int[heights.length];
// 使用 LinkedList 作为单调栈
LinkedList<Integer> list = new LinkedList<>();
// 遍历一遍高度数组来填充 left 数组
for (int i = 0; i < heights.length; i++) {
// 当栈不为空且当前柱子的高度小于等于栈顶柱子的高度时,不断弹出栈顶
while (!list.isEmpty() && heights[list.getLast()] >= heights[i]) {
list.removeLast();
}
// 若栈为空,说明当前柱子左侧没有更小的柱子,否则栈顶的位置即为左侧第一个小于当前柱子的位置
left[i] = list.isEmpty() ? -1 : list.getLast();
// 当前索引入栈
list.addLast(i);
}
list.clear(); // 清空栈,准备用于右侧小于当前柱子高度的索引查找
// 逆向遍历一遍高度数组来填充 right 数组
for (int i = heights.length-1; i >= 0; i--) {
while (!list.isEmpty() && heights[list.getLast()] >= heights[i]) {
list.removeLast();
}
// 若栈为空,说明当前柱子右侧没有更小的柱子,使用 heights.length 表示边界;否则,栈顶的位置即为右侧第一个小于当前柱子的位置
right[i] = list.isEmpty() ? heights.length : list.getLast();
// 当前索引入栈
list.addLast(i);
}
// 计算并更新最大矩形面积
int ans = 0;
for (int i = 0; i < heights.length; i++) {
// 对于每个柱子,计算以其为高的最大矩形面积,并更新最大值
ans = Math.max(heights[i] * (right[i] - left[i] - 1), ans);
}
return ans;
}
}
这段代码的核心在于使用两次单调栈遍历来分别确定每个柱子左侧和右侧第一个小于该柱子高度的柱子的索引。有了这两个索引,我们可以轻松计算出以当前柱子为高的矩形的最大宽度(即right[i] - left[i] - 1
),进而计算出面积。遍历所有柱子,我们就可以找到并返回最大的矩形面积。这种方法的时间复杂度为O(n),因为每个元素最多被压入和弹出栈一次。