单调栈一般用于求解“右边更大的数或更小的数”一类的问题,假如是求右边更大的数,需要保持栈顶到栈底是单调递增的,如果栈顶的值小于目前要入栈的元素,则弹出。
739. 每日温度
本题是要输出一个数组ans,ans[i]表示arr[i]之后比它更大的元素的下标 - i,如果不存在更大的元素,ans[i] = 0。
举例:[73, 74, 75, 71, 69, 72, 76, 73],输出[1, 1, 4, 2, 1, 1, 0, 0]。
这题算得上是单调栈的模板了,直接上代码:
/**
* @param {number[]} temperatures
* @return {number[]}
*/
var dailyTemperatures = function (temperatures) {
const len = temperatures.length;
const singleStack = [];
const ans = Array(len).fill(0);
for (let i = 0; i < len; i++) {
// 单调栈弹出元素
while (temperatures[i] > temperatures[singleStack.at(-1)]) {
const index = singleStack.pop();
// 这里的操作因题而异
ans[index] = i - index;
}
singleStack.push(i);
}
return ans;
};
496. 下一个更大元素I
本题有两个数组,nums1是nums2的子集。对于nums1中的每个数nums[i],在nums2中要先找到和他一样的数,再看后面有没有更大的,如果有,ans[i] = 这个数;如果没有,ans[i] = -1。
本题有以下要点:
- 单调栈用于求解nums2中“比当前数大的下一个数的下标”,存放在temp数组中,注意这次存放的不是 index - i。
- 如果遍历nums1时,每个数都要通过遍历nums2的方式去找到在nums2中的下标,无疑增加了时间复杂度,所以应该用map。
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var nextGreaterElement = function (nums1, nums2) {
const len1 = nums1.length;
const len2 = nums2.length;
const temp = Array(len2).fill(-1);
const ans = Array(len1).fill(0);
const singleStack = [];
const m = new Map();
for (let i = 0; i < len2; i++) {
while (nums2[i] > nums2[singleStack.at(-1)]) {
const index = singleStack.pop();
temp[index] = i;
}
singleStack.push(i);
m.set(nums2[i], i);
}
for (let i = 0; i < len1; i++) {
// const index = nums2.findIndex(item => item === nums1[i])
const index = m.get(nums1[i]);
ans[i] = temp[index] !== -1 ? nums2[temp[index]] : -1;
}
return ans;
};
503. 下一个更大元素II
本题只有一个数组,ans[i]存放比nums[i]更大的下一个元素,并且数组是首尾相连。
单调栈难度不大,难点在于怎么处理首尾相连。有两种方式:
- 把除了最后一个元素的数组 拼接到原数组后面。
- 模运算。
var nextGreaterElements = function (nums) {
const _nums = nums.concat(nums.slice(0, nums.length - 1));
const ans = Array(nums.length).fill(-1);
const len = _nums.length;
const singleStack = [];
for (let i = 0; i < len; i++) {
while (_nums[singleStack.at(-1)] < _nums[i]) {
const index = singleStack.pop();
ans[index] = _nums[i];
}
singleStack.push(i);
}
return ans.slice(0, nums.length);
};
我一开始就知道要用模,但是怎么用,还是想了一会。
var nextGreaterElements = function (nums) {
const len = nums.length;
const ans = Array(len).fill(-1);
const singleStack = [];
for (let i = 0, j = 0; i < 2 * len - 1; i++, j = i % len) {
while (nums[singleStack.at(-1)] < nums[j]) {
const index = singleStack.pop();
ans[index] = nums[j];
}
singleStack.push(j);
}
return ans;
};
42. 接雨水
双指针法,先用leftMax和rightMax分别记录i左侧和右侧最大的高度(包括自己),在i处,能积的雨水就等于 两侧高度最大值中的较小值 - 自身高度。这种做法是按列计算。
var trap = function(height) {
let sum = 0
const n = height.length
const leftMax = Array(n).fill(0)
const rightMax = Array(n).fill(0)
leftMax[0] = height[0]
rightMax[n - 1] = height[n - 1]
for (let i = 1; i < n; i++) {
leftMax[i] = Math.max(height[i], leftMax[i - 1])
}
for (let i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(height[i], rightMax[i + 1])
}
for (let i = 0; i < n; i++) {
sum += Math.min(leftMax[i], rightMax[i]) - height[i]
}
return sum
};
单调栈法,因为找到右侧比自己大的元素说明有凹槽,栈顶到栈底保持递减,如果遇到比栈顶大的值,就出栈,并且计算雨水。
遇到栈顶元素等于height[i]的时候,可以出栈,也可以不出。出栈是更新了了此时右边界的值,举例如下:
-- --
-- -- --
0 1 2 3 4
可见,中间有两个凹槽,能积的雨水为2,如果等于的情况出栈,则遇到i = 4时,栈里的情况是:[1, 3],h = Math.min(1, 1) - 0 = 1,w = 4 - 1 - 1 = 2,也就是当当前元素 > 栈顶元素时,while循环进行两轮。从这里也可以看出,单调栈是按行计算,当然不一定每次只算一行,如果雨水区域刚好构成了高大于1的矩形,一次也可以算多行。
如果等于的情况不出栈,则遇到i = 4时,栈是[1, 2, 3],第一次h = Math.min(1, 0) - 0 = 0, w = 1,第二次h = 1, w = 2,while循环进行了两轮。
所以等于的情况可以出栈,也可以不出。
var trap = function (height) {
let sum = 0;
const n = height.length;
const singleStack = [];
for (let i = 0; i < n; i++) {
while (height[singleStack.at(-1)] < height[i]) {
const index = singleStack.pop();
// 左边界还需要拿此时的栈顶,因此要判断栈不为空
if (singleStack.length > 0) {
const h = Math.min(height[i], height[singleStack.at(-1)]) - height[index];
const w = i - singleStack.at(-1) - 1;
sum += h * w;
}
}
singleStack.push(i);
}
return sum;
};