这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。
63. 每日温度 (daily-temperatures)
标签
- 单调栈
- 中等
题目
这里不贴题了,leetcode打开就行,题目大意:
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
相关知识
典型找下一个更佳值 NGE (Next Greater Element)问题, 只不过现在不是问你 Next Greater Number 是多少,而是问你当前距离 Next Greater Number 的长度而已。
单调栈在上一篇文章中有详细说明和例子,如果不清楚请移步 单调栈
基本步骤
我们还是根据单调栈原理,维护一个存储下标的单调栈,(我们还是设置一个 stack 放置 index 索引),从栈底到栈顶的下标对应的温度列表中的温度依次递减。如果一个下标在单调栈里,则表示尚未找到下一次温度更高的下标。
- 正向遍历温度列表。
- 对于温度列表中的每个元素
T[i],- 如果栈为空,则直接将 i 进栈,
- 如果栈不为空,则比较栈顶元素
prevIndex对应的温度T[prevIndex]和当前温度T[i],- 如果
T[i] > T[prevIndex],则将prevIndex移除,并将prevIndex对应的等待天数赋为i - prevIndex,重复上述操作直到栈为空或者栈顶元素对应的温度小于等于当前温度,然后将 i 进栈。
- 如果
为什么可以在弹栈的时候更新 res[prevIndex] 呢?因为在这种情况下,即将进栈的 i 对应的 T[i] 一定是 T[prevIndex] 右边第一个比它大的元素,试想如果 prevIndex 和 i 有比它大的元素,假设下标为 j,那么 prevIndex 一定会在下标 j 的那一轮被弹掉。
由于单调栈满足从栈底到栈顶元素对应的温度递减,因此每次有元素进栈时,会将温度更低的元素全部移除,并更新出栈元素对应的等待天数,这样可以确保等待天数一定是最小的。
写法实现
var dailyTemperatures = function(T) {
let [stack, res] = [[], new Array(T.length).fill(0)]
for (let i = 0; i < T.length; i++) {
while (stack.length && T[i] > T[stack[stack.length - 1]]) {
// 找到了 NGE ,记录下当前栈顶元素 prevIndex,当前位置 i 就是 prevIndex 的 NGE
let prevIndex = stack[stack.length - 1]
// 出栈
stack.pop()
// 求距离存入数组
res[prevIndex] = i - prevIndex
}
// 将当前 index 推入栈顶
stack.push(i)
}
return res
};
console.log(dailyTemperatures([55,38,53,81,61,93,97,32,43,78]))
64. 柱状图中最大的矩形 (largest-rectangle-in-histogram)
标签
- 单调栈
- 困难
题目
这里不贴题了,leetcode打开就行,题目大意:
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
相关知识
单调栈在上一篇文章中有详细说明和例子,如果不清楚请移步 单调栈
基本思路
这个思路来自于别人的题解,我觉得非常清晰,这边是原文
名词释义
-
高个阵营: 从左到右审视直方图,在没遇到下一个bar之前,就存在已有长方形,比较高的就是“高个阵营”
-
矮个阵营: 和高个阵营相反 高矮阵营各有优势
-
遇到一个新的 bar 时
- 高个阵营兼容不了它,还因为矮 bar 的 “阻隔”,自此面积停止变大
- 矮个阵营可以兼容它,长度因此变长,面积变大
- 矮个阵营矮,但兼容性好,通过长度变长,变得更强
- 高个阵营高,但兼容性差,遇高则强,遇矮则生长停滞 高 bar 矮 bar 所带来的不同意义
-
来了个高 bar ,两个阵营都能补强。此时不需要计算面积,因为面积在变大,没有到最大
-
来了个矮 bar ,高个阵营面积不能再变大了,矮个阵营有逆袭的可能。考察高个阵营已无意义,算完面积即可抛弃,继续观察矮个阵营 问题来了,高矮是相对的
-
没有绝对的高 bar ,也没有绝对的矮 bar,没有绝对的高个阵营,也没有绝对的矮个阵营
-
难道每次都要根据一个新 bar 去区分高矮阵营吗?显然不实际 高矮,是通过比较产生的
-
举个例子:小王,军训集合迟到了,姗姗来迟
-
如果此时队伍参差不齐,很难比较出谁比小王高,谁比小王矮
-
如果队伍按高矮排好,小王从队头一个个地比,大家很容易知道自己和小王谁高
-
小王也很容易就知道自己该排到哪里
让比较的过程更快
- 将高矮排好(维护一个单调序列),迎接新 bar 的到来
- 问题来了,维护单调栈,还是单调队列?单调递增,还是单调递减? 为什么是单调递增?为什么是栈?
- 如果是单调递减栈
- 新 bar 比栈顶矮,入栈,维持递减性
- 新 bar 比栈顶高,靠近栈顶的矮个阵营会补强,继续考察,排后面的高个停止生长,需要抛弃
- 可是高个阵营不在栈顶,不好出栈啊
- 那,队列可以吗?让高个从队尾出列?
- 好的,但是,人家新 bar 要进来啊,要从高个出列的地方入列,才能保持单调性,这就不是队列了,是栈了
- 所以,只能是单调递增栈
- 新 bar 比栈顶高,入栈,保持单增性
- 新 bar 比栈顶低,栈顶的高个阵营停止生长,出栈,后头的矮个阵营留待观察 栈的单调性,谁去谁留,变得清晰
- 高个阵容遇到矮 bar 而停止发育,计算它形成的长方形面积后,就出栈
- 栈是单调递增的,不断将新的栈顶 bar 和当前 bar 比较,高的就走
- 等到栈顶 bar 不再高于当前 bar,就不再出栈,并让当前 bar 入栈 单调栈记录什么?
- bar 的位置(索引),
- 高度通过
height[i]求出 - 宽度通过索引相减求出
- 高度通过
基本步骤
- 维护一个 stack 栈。遍历 heights 数组的每一个 bar
- 当前 bar 比栈顶的 bar 高,直接入栈
- 当前 bar 比栈顶的 bar 矮:
- 栈顶元素(索引)出栈,暂存给
stackTopIndex变量 - 计算以
heights[stackTopIndex]为高的长方形的面积,宽度 = 当前 bar 的索引 i - 新的栈顶索引 - 1 ,与全局的最大比较
- 栈顶元素(索引)出栈,暂存给
- 当前 bar 继续和新的栈顶比较,重复上面过程,直到当前 bar 不再比栈顶的 bar 矮,入栈
下面是边界情况的分析
栈空了,面积公式就没法用了
- 求长方形的宽度,需要新的栈顶,如果没有呢?当栈只有一个元素,栈顶出栈,栈就空了
- 我们再思考另一个问题:让 heights 数组的索引 0 入栈,依据是什么?
- 入栈的依据是当前 bar 比栈顶 bar 高。问题是现在没有栈顶可以比较
- 我们可以
设立一个高为 0 的虚拟 bar,放在 heights 的 0 位置,它不影响结果,却可以让第一条 bar 的索引,名正言顺地入栈 - 同时解决了第一个问题:不会有别的 bar 比它更矮了,因此该 bar 永不出栈 最后一个 bar 需要解救
- 最后一个 bar 不会遇到新 bar 了,如果它在栈中,那就没有机会出栈了,意味着,没有机会计算栈中的长方形面积了
- 我们设立一个虚拟的高为 0 的 bar,放在 heights 数组的最右,栈中的 bar 都比它高,能一一出栈,得到解救
写法实现
var largestRectangleArea = function(heights) {
if (!heights || !heights.length) {
return 0;
}
// 两端添加0 巧妙处理下面遍历while的终止条件
heights.unshift(0)
heights.push(0)
// 获取栈顶元素的纯函数
const stackTopItem = () => stack[stack.length - 1]
let [maxArea, stack] = [0, []]
for (let i = 0; i < heights.length; i++) {
// stack 有值 且当前bar比栈顶bar矮
while (stack.length && heights[stackTopItem()] > heights[i]) {
// 栈顶元素出栈,并保存栈顶bar的索引
const stackTopIndex = stack.pop()
// 得到与左侧最近项最大宽度
const width = i - stackTopItem() - 1
// 计算面积,取最大值
maxArea = Math.max(maxArea, width * heights[stackTopIndex] )
}
// 当前bar比栈顶bar高了,入栈
stack.push(i)
}
return maxArea
};
console.log(largestRectangleArea([2,1,5,6,2,3]))
另外向大家着重推荐下这位大哥的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列
今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦
搜索我的微信号infinity_9368,可以聊天说地
加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我
presious tower shock the rever monster,我看到就通过,暗号对不上不加哈,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧