可以把单调队列视为单调栈的升级版:
- 单调栈维护
[0, i)
区间内的单调序列及其最大/最小值(因而我们说单调栈维护各元素的左侧/右侧第一个比自己大/小的数);- 而单调队列还能够弹出队首的元素,因此维护
[lastpop, i)
区间内的单调序列及其最大/最小值,即左边界可以变动,因而可以维护滑动窗口的最值
下一个更大元素:给出一个数组,返回一个等长数组,其中各索引出存储着原数组的该处元素的下一个更大元素(不存在则为-1),例如,输入
[2,1,2,4,3]
,返回[4,2,4,-1,-1]
如何用O(n)复杂度解决问题?(相当于只线性扫描一遍数组)
思路:把数组元素想象成站成一队的人,前面的人往后看,视线向上,第一个可见的人就是下一个更大元素(矮的都被高的挡住了,只看得到比自己高、更高、更更高的人…)
单调栈就是模拟这个过程:
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
"""寻找下一个更大元素
从后往前扫描,同时维护单调栈
模拟每个元素向后看,只能看到高度依次递增的人
当前元素将高度小于自己的人全部出栈,栈顶就留下下一个更大元素"""
ans = [None for _ in range(len(nums))] # 答案
stk = [] # 单调栈
for i in range(len(nums) - 1, -1, -1):
# 当前元素为nums[i]
while len(stk) > 0 and stk[-1] < nums[i]: # 当前元素将高度小于自己的人全部出栈
stk.pop()
ans[i] = stk[-1] if len(stk) > 0 else -1 # 栈顶元素就是下一个更大元素
stk.append(nums[i]) # 入栈
return ans
- LeetCode 496. 下一个更大元素 I(模板题)
- LeetCode 739. 每日温度(模板题,输出下一个更大元素的位置,栈中变为保存下标)
- LeetCode 901. 股票价格跨度(类似上题,维护上一个更大元素的下标,返回两者之间的距离,用虚拟下标-1处理没有上一个更大元素的特殊情况,称为哨兵)
- LeetCode 503. 下一个更大元素 II(题目变为循环数组,可以物理上拼接一个两倍长的数组/在逻辑上求模)
升级变式:
- LeetCode 581. 最短无序连续子数组(隐含的单调栈,希望找出一个子数组,其左侧所有元素 <= 子数组内所有元素 <= 其右侧所有元素,①从左往右遍历,寻找第一个有【右侧下一个更小元素】的位置,则这里是左边界②从右往左遍历,寻找第一个有【左侧下一个更大元素】的位置,则这里是右边界)
- LeetCode 84. 柱状图中最大的矩形(求柱形图中能勾勒出的最大面积,依次枚举每个柱子的高度作为矩形高度,并尽可能向左右延伸,利用单调栈遍历两次求出[某位置左侧/右侧第一个比他小的柱子下标],两者之差作为宽)
ps. 题目数据改为二维01矩阵形式,求最大全1矩形面积,就是【最大子矩阵的大小】的问题,同样可以转为柱形图的问题来解决- LeetCode 316. 去除重复字母/402. 移掉 K 位数字(隐含的单调栈问题,要获得字典序最小/数字最小的子序列,则应该尽量保证序列靠前的的部分单调递增)
LeetCode 402. 移掉 K 位数字
给出一个数组,从中挑选k个数字,构成数字序列,求字典序最小的可能序列
最小字典序原则:对于11a5和11b5,前面的数字确定,则当前位上,选择a和b中更小的,字典序也一定最小
思路:
class Solution:
def mostCompetitive(self, nums: List[int], k: int) -> List[int]:
"""同LeetCode 402. 移掉 K 位数字
要保留k个,等效于删除L-k个"""
# 对于11a5和11b5,前面的数字确定,则当前位上,选择a和b中更小的那个一定更好
# 从左往右遍历,维护数字序列->序列单调递增最好
# 每次有新数字,考虑是否要丢弃前面的更大数->单调栈
# 最多只能删除L-k次, 将L-k次机会用完后,再后面的数字只能直接拼接
stk = []
del_chance = len(nums) - k
for i, n in enumerate(nums):
# 入栈前删除所有更大的
while del_chance > 0 and stk and stk[-1] > n:
stk.pop()
del_chance -= 1
stk.append(n)
return stk[:k]
LeetCode 316. 去除重复字母
取出字符串s中的重复字符,返回所有可能结果中字典序最小的
分析:
首先,最理想的“最小字典序”是abcdefg...
,实际情况肯定不是这样,但是我们能发现构造答案的策略:如果可以的话,应该尽量让字母递增排列,从而满足“最小字典序”的特性(例如abcd
字典序小于abdc
,后者出现了“递减”,相对而言字典序更大的字符d
出现在了前面)
因此,我们的策略是:从左到右遍历,尽可能构造“递增”的序列 -> 单调栈
这样看来,这题就和之前的问题有相似之处,区别在于这里要保证每个字符出现且仅出现1次,因此入栈前的出栈有限制:仅当后面还有同样的字符时,才能将前一个字符出栈
abca
的最后一个a
,前面已是递增的最小字典序排列)from collections import Counter
class Solution:
def removeDuplicateLetters(self, s: str) -> str:
"""同LeetCode 402. 移掉 K 位数字"""
# 前面的数字确定,则当前位上,字母更小的那个一定更好
# 维护单调栈,有新的字母入栈时:若前一个字符比他大,应该考虑丢掉前一个
# (但注意前提:前一个字符在后面还有出现,才能丢弃)
cnt = Counter(s) # 记录当前位置之后,某个字符还会出现几次
inStk = set()
stk = []
for i, ch in enumerate(s):
# 当前字符入栈有限制:前面没有这个字符,才能入栈(考虑bcab的后一个b)
if ch not in inStk:
# 入栈前删除所有更大的
while stk and stk[-1] > ch:
pre_ch = stk[-1]
if cnt[pre_ch] > 0: # 后面还有出现,可以删除
stk.pop()
inStk.remove(pre_ch)
else: # 不能删除
break
stk.append(ch)
inStk.add(ch)
cnt[ch] -= 1
return ''.join(stk)
LeetCode 321. 拼接最大数
给出两个数组,从这两个数组中选出 k个数字,拼接成一个新数组,求可能的字典序最大的数组(拼接时,同一数组内的相对位置要保持不变)
如nums1 = [3, 4, 6, 5],nums2 = [9, 1, 2, 5, 8, 3],k = 5
返回[9, 8, 6, 5, 3]
基础:Python提供数组的大小比较,比较大小标准也是字典序
如[0,1,0]<[0,1,7],[]<[1]
分析:
class Solution:
def maxNumber(self, nums1: List[int], nums2: List[int], k: int) -> List[int]:
# 将取k个拆分:从nums1中取i个+nums2中取k-i个,然后将两个结果合并为最大的
# 尝试所有可能,取最大值
def chooseNum(nums, k):
"""从nums中取k个数,使得其最大(等价于删除L-k个数)
同LeetCode 1673. 找出最具竞争力的子序列"""
if k == len(nums):
return nums
stk = []
del_chance = len(nums) - k
for i, n in enumerate(nums):
# 入栈前删除所有更大的
while del_chance > 0 and stk and stk[-1] < n:
stk.pop()
del_chance -= 1
stk.append(n)
return stk[:k]
def merge(n1, n2):
"""合并两个数组,得到最大的答案
策略:每次取出最大的拼接"""
res = []
l1, l2 = len(n1), len(n2)
i, j = 0, 0
while i < l1 and j < l2:
if n1[i] > n2[j]:
res.append(n1[i])
i += 1
elif n1[i] < n2[j]:
res.append(n2[j])
j += 1
else: # 两数相等,应该看后面的部分,优化揭露更大的(如[0,1,0]<[0,1,7],应取后一个)
if n1[i:] > n2[j:]:
res.append(n1[i])
i += 1
else:
res.append(n2[j])
j += 1
if i < l1:
res += n1[i:]
if j < l2:
res += n2[j:]
return res
L1, L2 = len(nums1), len(nums2)
ans = []
for i in range(0, k + 1):
j = k - i
if i <= L1 and j <= L2:
now = merge(chooseNum(nums1, i), chooseNum(nums2, j))
if not ans or now > ans:
ans = now
return ans