0. 前言
最新Leetcode刷题记录:84题、85题,还有前面的42题。它们都可以采用相同的方法来做 —— 单调栈。今天,小编就来跟大家聊一聊聊一聊单调栈。
1. 单调栈
我们知道,栈是一种非常简单的数据结构,它使用先进后出的逻辑进行排序。那么,什么是单调栈呢?单调栈的本质还是栈,只不过用了一些巧妙地逻辑,使得栈中的元素始终都保持有序。单调栈可以解决的最原始的问题是: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大的数,3后面没有比3大的数,这两个位置填-1.
拿到这个题目,首先能想到的是暴力,我们依次遍历数组某个元素之后的数,找到那个比它大的数就将这个数填到对应的位置,如果没有找到就填写-1.
用暴力解决的代码如下:
var findNextGreaterElement = function(nums) {
const n = nums.length;
const ans = new Array(n).fill(-1);
for(let i = 0; i < n; i++) {
for(let j = i + 1; j < n; j++) {
if(nums[j] > nums[i]) {
ans[i] = nums[j];
break;
}
}
}
return ans;
}
虽然暴力的解法很容易想到,但是它的时间复杂度是O(n2)。如果我们换种思路来解决,那么只需要用O(n)的时间复杂度就得到问题的答案。
我们利用单调栈来解决这个问题:首先,我们定义一个栈(反映在JavaScript代码中就是一个数组),然后对元素的大小进行比较。栈顶元素小于等于当前元素时,弹栈;否则,入栈,并且将栈顶元素放入到数组中。废话不多说,先贴代码:
var findNextGreaterElement = function(nums) {
const n = nums.length;
const ans = new Array(n);
const stack = [];
for(let i = n - 1; i >= 0; i--) {
while(stack.length && stack[stack.length - 1] <= nums[i]) {
stack.pop();
}
ans[i] = stack.length === 0 ? -1 : stack[stack.length - 1];
stack.push(nums[i]);
}
return ans;
}
由于我们是从后往前遍历数组的,栈中保存的元素始终位于当前判定元素之后。也就是说,我们遍历到第 i 个元素的时候,我们已经把该元素后面的 n - i 个元素遍历完了。栈中保存的是对之前的元素遍历的结果。我们只需要把比当前遍历的元素小的那些元素剔除掉,留在栈顶的那个元素就是我们需要的Next Greater Element。
然后,我们再来分析这个算法的时间复杂度。对于输入的数组中的每一个元素,它们最多只能入栈一次,也只能出栈一次。因此,这个算法的规模和数组元素的规模是成正比的,因此,时间复杂度为O(n)。
2. 单调栈解题模板
在前面的内容中,我们已经利用单调栈解决了一个最简单的问题了。在这部分,我们就来整理一下单调栈的解题模板。废话不多说,直接上代码:
var monotoneStackTemplate = function (param) {
// 一般来讲,这类型问题输入的参数是一个数组,其实不是数组也没关系。只要有数据就行。。
// 我们首先来定义所需要的栈和保存结果的数组
// 保存结果的数组规模和输入的数组元素个数相同
const ans = new Array(n);
const stack = [];
// 然后依次遍历输入数组的元素
// 在遍历的时候,可以倒着入栈,也可以正着入栈。只要记清楚倒着入栈,正着出栈;正着入栈,倒着出栈即可
for(let i = n - 1; i >= o; i--) {
// 判断何时出栈,何时入栈的条件也不是固定的,只要记得让不满足条件的元素出栈,满足条件的留在栈中就行了。
// 这里需要特别注意的是在进行pop操作的时候,需要保证栈不为空
while(stack.length && stack[stack.length - 1] <= param[i]) {
// 注意,这里面的操作也不是固定的,需要根据实际的情况来进行改变
stack.pop();
}
ans[i] = stack.length === 0 ? -1 : stack[stack.length - 1];
stack.push(param[i]);
}
// 返回需要的结果就行了,这里不一定要返回 ans
return ans;
}
这个模板可以用来解决任何,查找下一个更好的元素的问题。只要我们可以把问题抽象成,查找下一个更好的元素,都可以用它来解决。注意,一定是查找下一个比当前元素更好的元素。重要的事情说三遍!!!
3. leetcode 部分相关题目
在文章的最后,我们利用这个模板来解决三个Leetcode题目,分别是:
- 42题:接雨水
- 84题:柱状图中最大的矩形
- 85题:最大矩形
3.1 接雨水
我们知道,要让水存起来,需要形成一个凹槽。抽象成数学的概念就是形成一个先单调递减然后再单调递增的一条曲线,就可以存积水。因此,在这个题目中,我们需要一个单调递减的单调栈。按照单调栈的模板,不满足单调递减的条件需要弹栈。但是在这个题目中,并不是简单的进行弹栈就可以的,除了弹栈之外,还需要计算存水的面积。计算积水的面积,我们需要三个参数,最底端的一个下界,还需要根据木桶原理计算上届。
/**
* @param {number[]} height
* @return {number}
*/
var trap = function(height) {
const n = height.length;
const stack = [];
let ans = 0;
for(let i = 0; i < n; i++) {
while(stack.length && height[stack[stack.length - 1]] < height[i]) {
const top = stack.pop();
if(!stack.length) {
break;
}
const h = Math.min(height[i], height[stack[stack.length - 1]]) - height[top];
const w = i - stack[stack.length - 1] - 1;
ans += h * w;
}
stack.push(i);
}
return ans;
};
3.2 柱状图中最大的矩形
因为我们要计算面积,所以如果以某个元素的高度为基准的话,那么可以计算到面积的一定是比当前元素高的矩形才可以。因此,我们就左边找到所有比当前元素高的,右边也找到所有比当前元素高的,然后相减,再乘当前高度。在所有计算得到的面积了。寻找所有比当前元素高的,等价于寻找下一个比当前元素低的元素。因此,代码如下:
/**
* @param {number[]} heights
* @return {number}
*/
var largestRectangleArea = function(heights) {
const n = heights.length;
const left = new Array(n), right = new Array(n);
let stack = [];
let ans = 0;
for(let i = 0; i < n; i++) {
while(stack.length !== 0 && heights[stack[stack.length - 1]] >= heights[i]) {
stack.pop();
}
left[i] = stack.length === 0 ? -1 : stack[stack.length - 1]
stack.push(i);
}
stack = [];
for(let i = n - 1; i >= 0; i--) {
while(stack.length !== 0 && heights[stack[stack.length - 1]] >= heights[i]) {
stack.pop();
}
right[i] = stack.length === 0 ? n : stack[stack.length - 1]
stack.push(i);
}
for(let i = 0; i < n; i++) {
ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
};
3.3 最大矩形
这个题目的难点在于需要自己构建上一个题目的矩形,其实和上一个是类似的思路:
/**
* @param {character[][]} matrix
* @return {number}
*/
var maximalRectangle = function(matrix) {
const m = matrix.length;
if(m === 0) return 0;
const n = matrix[0].length;
const cnt = new Array(m).fill(0).map(() => new Array(n).fill(0));
for(let i = 0; i < m; i++) {
for(let j = 0; j < n; j++) {
if(matrix[i][j] === '1') {
cnt[i][j] = (j === 0 ? 0 : cnt[i][j - 1]) + 1;
}
}
}
let ret = 0;
for(let i = 0; i < n; i++) {
const up = new Array(m).fill(0);
const down = new Array(m).fill(0);
let stack = [];
for(let j = 0; j < m; j++) {
while(stack.length && cnt[stack[stack.length - 1]][i] >= cnt[j][i]) {
stack.pop();
}
up[j] = stack.length === 0 ? -1 : stack[stack.length - 1];
stack.push(j);
}
stack = [];
for(let j = m - 1; j >= 0; j--) {
while(stack.length && cnt[stack[stack.length - 1]][i] >= cnt[j][i]) {
stack.pop();
}
down[j] = stack.length === 0 ? m : stack[stack.length - 1];
stack.push(j);
}
for(let j = 0; j < m; j++) {
const height = down[j] - up[j] - 1;
const area = height * cnt[j][i];
ret = Math.max(area, ret);
}
}
return ret;
};