LeetCode——单调栈题型分析
〇、引入
什么是单调栈:单调栈是栈结构,但使用了一定方法让栈内元素保持单调性(单调递增或递减)。
能解决的问题:专门解决 Next Greater Element 这类典型问题。即在数组中寻找每个元素的下个更大元素。
本篇主要描述了什么是 Next Greater Element问题、单调栈的解题模板,以及它能解决的哪些衍生问题。
一、Next Greater Element
描述:
给定一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -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。
题解:
对于上面这种类型的题最优的解决方式就是单调栈了,个人认为单调栈思想是:“暂时得不出结果的数据就入栈,等待时机到了,能得出结果时再取出来处理。”一般来说,每个元素都不可能在入栈时就能得出结果,所以每个元素都会有入栈操作,只是每个元素入栈前需要判断之前栈顶的元素是否等到了时机,只要时机到了就把之前存入栈顶的元素取出处理,而这个时机也控制着栈内元素的单调性。
比如循环示例中的数组,
遍历到下标为0的元素时,此时并不知道下个更大元素是谁,所以将当前下标入栈,栈内元素有[0],栈顶为0;
遍历到下标为1的元素时检查栈顶的元素是否等到了时机,一经比较1不大于arr[0]=2,说明栈顶元素时机并没有到,然后将当前下标入栈,栈内元素有[0,1],栈顶为1;
遍历到下标为2的元素时检查栈顶的元素是否等到了时机,一经比较2大于arr[1]=1,说明栈顶元素等到了时机,则弹出栈顶元素计算出对应的结果值 result[1]=2,之后栈内元素有[0],栈顶为0,再比较2不大于arr[0]=2,然后将当前下标入栈,栈内元素有[0,2],栈顶为2;
遍历到下标为3的元素时检查栈顶的元素是否等到了时机,一经比较4大于arr[2]=2,说明栈顶元素等到了时机,则弹出栈顶元素计算出对应的结果值 result[2]=4,之后栈内元素有[0],栈顶为0,再比较4大于arr[0]=2,说明栈顶元素等到了时机,则弹出栈顶元素计算出对应的结果值 result[0]=4,,然后将当前下标入栈,栈内元素有[3],栈顶为3;
遍历到下标为4的元素时检查栈顶的元素是否等到了时机,一经比较3不大于arr[3]=4,说明栈顶元素时机并没有到,然后将当前下标入栈,栈内元素有[3,4],栈顶为4;
但此时数组遍历结束了,还有栈内两个元素没有处理,于是在最后再来处理一下栈内剩余的元素,分别给他们下标对应值存-1,到此就处理结束了。
复杂度分析:
共n个元素,用for循环遍历一次数组,用while循环对每个元素进行了一次入栈和出栈,所以时间复杂度为O(n),空间复杂度为O(n)。
参考模板代码
public int[] fun1(int[] arr) {
// 1.声明栈、结果集
Stack<Integer> stack = new Stack<>();
int[] result = new int[arr.length];
// 2.遍历数组(或线性结构的容器)
for (int i = 0; i < arr.length; i++) {
// 3.单调栈相关处理
// 3.1 判断当前元素入栈前栈中是否有元素可以处理了
while (!stack.empty() && arr[i] > arr[stack.peek()]) {
// 3.2 将现在能处理的元素取出(这步维护着栈的单调性)
int index = stack.pop();
// 3.3 计算取出元素对应的值
result[index] = arr[i];
}
// 3.4 当前下标入栈
stack.push(i);
}
// 3.5 处理栈中没有处理完元素对应的结果值(如果该值在初始化结果集时处理了这儿就不用处理)
while (!stack.empty()){
int prevIndex = stack.pop();
result[prevIndex] = -1;
}
return result;
}
二、解决的衍生问题
2.1、LeetCode496.下个更大元素I(简单)
描述:
给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。 nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
示例 1: 输入: nums1 = [4,1,2], nums2 = [1,3,4,2]. 输出: [-1,3,-1] 解释: 对于num1中的数字4,你无法在第二个数组中找到下一个更大的数字,因此输出 -1。 对于num1中的数字1,第二个数组中数字1右边的下一个较大数字是 3。 对于num1中的数字2,第二个数组中没有下一个更大的数字,因此输出 -1。
示例 2: 输入: nums1 = [2,4], nums2 = [1,2,3,4]. 输出: [3,-1] 解释: 对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。 对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
提示:
nums1和nums2中所有元素是唯一的。
nums1和nums2的数组大小都不超过1000。
题解:
遍历nums2,根据单调栈模板计算出nums2中所有元素的下个更大元素值并存在map中;
遍历nums1,从map中取出对应的值即可。
代码展示:
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 1.声明栈、结果集
Stack<Integer> stack = new Stack<>();
Map<Integer,Integer> resMap = new HashMap<>();
// 2.遍历数组(或线性结构的容器)
for (int i = 0; i < nums2.length; i++) {
// 3.单调栈相关处理
// 3.1 判断当前元素入栈前栈中是否有元素可以处理了
while (!stack.empty() && nums2[i] > nums2[stack.peek()]) {
// 3.2 将现在能处理的元素取出(这步维护着栈的单调性)
int index = stack.pop();
// 3.3 计算取出元素对应的值
resMap.put(nums2[index],nums2[i]);
}
// 3.4 当前下标入栈
stack.push(i);
}
// 遍历nums1,从map中取出对应的值
for (int i = 0; i < nums1.length; i++) {
nums1[i] = resMap.getOrDefault(nums1[i],-1);
}
return nums1;
}
2.2、LeetCode503.下个更大元素II(中等)
描述:
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
示例 1:
输入: [1,2,1] 输出: [2,-1,2] 解释: 第一个 1 的下一个更大的数是 2; 数字 2 找不到下一个更大的数; 第二个 1 的下一个最大的数需要循环搜索,结果也是 2。 注意: 输入数组的长度不会超过 10000。
题解:
采用的循环数组,我们可以在遍历数组时直接把数组下标扩大一倍来达到循环的效果,在数组中存取值时用取模的方式防止下标越界就OK了。
代码展示:
public int[] nextGreaterElements1(int[] nums) {
// 1.声明栈、结果集
Stack<Integer> stack = new Stack<>();
int[] result = new int[nums.length];
Arrays.fill(result, -1);
// 2.遍历数组(或线性结构的容器)
for (int i = 0,numsLength = nums.length; i < numsLength * 2; i++) {
// 3.单调栈相关处理
// 3.1 判断当前元素入栈前栈中是否有元素可以处理了
while (!stack.empty() && nums[i % numsLength] > nums[stack.peek()]) {
// 3.2 将现在能处理的元素取出(这步维护着栈的单调性)
int index = stack.pop();
// 3.3 计算取出元素对应的值
result[index] = nums[i % numsLength];
}
// 3.4 当前下标入栈,循环处理需要模上数组长度
stack.push(i % numsLength);
}
return result;
}
2.3、LeetCode41.接雨水(困难)
描述:
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1] 输出: 6
题解:
接的雨水量取决于水洼的大小,而水洼的大小由两边最高点和中间最低减去洼槽高度,再乘以左右边际的宽度决定。所以我们可以构造一个单调栈(递减),遇到非递减就计算出当前水平面容纳水的大小,最后依次将这些大小相加就能得到总的容量大小。注意这儿将每个水洼的容量按照水平面拆分成若干个矩形计算,水平面取决于每个递增和递减值之间的最大高度决定。
代码展示:
public int trap(int[] height) {
// 1.声明栈、结果集
Stack<Integer> stack = new Stack<>();
int result = 0;
// 2.遍历数组(或线性结构的容器)
for (int i = 0; i < height.length; i++) {
// 3.单调栈相关处理(构造一个单调递减栈)
// 3.1 判断当前元素入栈前栈中是否有元素可以处理了
while (!stack.empty() && height[i] > height[stack.peek()]) {
// 3.2取出栈顶元素,当做中间低处的洼槽(维护着单调性)
int bottom = stack.pop();
// 需要有栈是否为空的判断,防止没有左边界时抛异常
if(stack.isEmpty()){
continue;
}
int left = stack.peek();
// 3.3 计算当前柱子与前面柱子围的洼槽容积,
// 取两边短中的那个柱子高度减去洼槽高度作为高,下标距离-1作为宽
result += (Math.min(height[i], height[left]) - height[bottom]) * (i - left - 1);
}
// 当前元素下标入栈
stack.push(i);
}
return result;
}


浙公网安备 33010602011771号