单调栈是栈的一种,可以使得每次新元素入栈后,栈内的元素都保持有序(单调递增或者单调递减)。
单调栈的用途不太广泛,只处理一类典型的问题,比如[下一个更大元素]、[上一个更小元素] 等。
在本文中,我将首先介绍 [单调栈] 的使用模板,接着我会使用单调栈的技巧来解决力扣hot100中的两道题:739、每日温度;84、柱状图中最大的矩形
单调栈的使用
例题:输入一个数组
nums
,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。
函数签名如下:
int[] nextGreaterElement(int[] nums);
比如说,输入一个数组 nums = [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。
int[] nextGreaterElement(int[] nums) {int n = nums.length;// 存放答案的数组int[] res = new int[n];Stack<Integer> s = new Stack<>(); // 倒着入栈,借助栈的结构,就是正着出栈for (int i = n - 1; i >= 0; i--) {// 对元素进行判定:如果当前元素不小于栈顶元素,则弹出//因为我们要找的是“最近的更大元素”while (!s.isEmpty() && s.peek() <= nums[i]) {s.pop();}// nums[i] 身后的更大元素//如果为-1,说明没有找到合适的元素res[i] = s.isEmpty() ? -1 : s.peek();s.push(nums[i]);}return res;
}
我们用文字来描述一下这个过程:
由于是倒着入栈,所以我们从 [3] 开始。[3] 后面没有比它大的元素,返回 -1,然后将 [3] 入栈;
接着是 [4] ,此时栈不为空,栈顶元素(3)小于 [4] 被弹出,栈被置为空。所以 res[i] = -1,将 [4] 压入栈中。
接着是 [2],此时栈不为空,栈顶元素 [4] 大于 [2] 满足条件,将其记录到res数组中,然后将 [2] 压入栈中。
接着是 [1],此时栈不为空,栈顶元素 [2] 大于 [1],满足条件,将其记录到res数组中,然后将 ]1\ 压入栈中。
最后是 [2],此时栈顶元素 [1] 小于 [2],弹出;弹出后,新的栈顶元素是 [2] ,不符合条件,弹出;新的栈顶元素变为 [4],满足条件,将其记录下来。并将 [2] 压入栈中
数组nums遍历结束,返回res数组。
问题1:为什么要倒着入栈?
从后往前入栈,保证了出栈的时候,栈顶元素一定是距离当前目标元素最近的元素。这样能实现寻找 “最近的更大元素”/"下一个更大元素"
比如数组 [1,2,3,4] 。当数组倒着遍历到2的时候,栈中的元素是 [4,3],那么出栈顺序就是 [3,4]。就是这样借助栈的性质来实现查找
力扣739:每日温度
比如说给你输入 temperatures = [73,74,75,71,69,76]
,你返回 [1,1,3,2,1,0]
。因为第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温,后面的同理。
这个问题本质上也是找下一个更大元素,只不过现在不是问你下一个更大元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离而已。
所以我们要对上面单调栈的算法模板稍加修改:之前用于存放元素的数组res中不再用于记录元素,而是记录元素的索引
int[] dailyTemperatures(int[] temperatures) {int n = temperatures.length;int[] res = new int[n];// 这里放元素索引,而不是元素Stack<Integer> s = new Stack<>(); /* 单调栈模板 */for (int i = n - 1; i >= 0; i--) {while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {s.pop();}// 得到索引间距res[i] = s.isEmpty() ? 0 : (s.peek() - i); // 将索引入栈,而不是元素s.push(i); }return res;
}
栈 s 中存放的是元素索引,所以当栈顶元素满足条件——温度高于当天温度时,栈顶元素意为:在这一天达到下一个更高的温度。
然后用栈顶元素减去当日日期就是相隔几天。
力扣84:柱状图中最大的矩形
对于该问题,暴力解法的思路是枚举每个柱子作为矩形的高度,并以该柱子为中心向左右延伸,直到遇到高度小于当前柱子的柱子为止。然后计算以当前柱子为高度的矩形的面积,并更新最大面积。
具体的实现步骤如下:
- 遍历每个柱子,将其作为矩形的高度(记为h)。
- 从当前柱子向左延伸,直到遇到高度小于h的柱子为止,记录左边界的位置(记为left)。
- 从当前柱子向右延伸,直到遇到高度小于h的柱子为止,记录右边界的位置(记为right)。
- 计算以当前柱子为高度的矩形的面积,即面积 = h * (right - left - 1)。
- 更新最大面积,如果当前面积大于最大面积,则更新最大面积。
- 重复以上步骤,直到遍历完所有柱子。
暴力方法的时间复杂度为 O(N^2),会超出时间限制,主要在于寻找左右边界上。而寻找左右边界,其实就是 “寻找最近的高度小于h的柱子”。可以使用单调栈解决。
需要注意的是,根据题目要求,我们要分别寻找左边界和右边界,所以我们要使用两次单调栈。
class Solution {public int largestRectangleArea(int[] heights) {int n = heights.length;int[] left = new int[n]; // 存储每个柱子左边第一个小于它的柱子的索引int[] right = new int[n]; // 存储每个柱子右边第一个小于它的柱子的索引Deque<Integer> mono_stack = new ArrayDeque<Integer>(); // 单调栈,存储柱子的索引//我们寻找右边界时是倒着入栈,那么寻找左边界时就是正着入栈for (int i = 0; i < n; ++i) {//在这里我们要寻找的是:当前柱子左侧的第一个小于它的索引。所以如果栈顶元素大于当前柱子高度(不符合条件),则弹出该元素while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {mono_stack.pop(); }left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek()); mono_stack.push(i); // 将当前柱子的索引入栈}//清除栈中的元素,因为后面还要用这个栈mono_stack.clear();for (int i = n - 1; i >= 0; --i) {while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {mono_stack.pop(); }right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek()); mono_stack.push(i); // 将当前柱子的索引入栈}int ans = 0;for (int i = 0; i < n; ++i) {// 计算每个柱子的面积并更新最大面积ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]); }return ans;}
}