单调栈:栈内的元素按照某种方式排序下单调递增或单调递减,如果新入栈的元素破坏的单调性,就弹出栈内元素,直到满足单调性。
单调栈分为单调递增栈和单调递减栈:
维护单调递增栈:
for i in range(len(nums)):
while stack and nums[i]<=stack.top():
stack.pop()
stack.push()
维护单调递减栈:
for i in range(len(nums)):
while stack and nums[i]>=stack.top():
stack.pop()
stack.push()
1. 求第 i i i 个数左边第一个比它小的元素的位置
举例来说, n u m s = [ 5 , 4 , 3 , 4 , 5 ] nums=[5,4,3,4,5] nums=[5,4,3,4,5],初始时栈空 s t a c k = [ ] stack=[] stack=[]
2. 求第 i i i 个数左边第一个比它大的元素的位置
3. 求第 i i i 个数右边第一个比它小的元素的位置
举例来说, n u m s = [ 5 , 4 , 3 , 4 , 5 ] nums=[5,4,3,4,5] nums=[5,4,3,4,5],初始时栈空 s t a c k = [ ] stack=[] stack=[]
4. 求第 i i i 个数右边第一个比它大的元素的位置
当然,你也可以构造一个单增栈同时维护元素左边和右边第一个比它小的元素,或者,构造一个单减栈同时维护元素左边和右边第一个比它大的元素。
LeetCode传送门
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
思路:
问题转化:求元素右边第一个比它大的元素下标。可以维护一个存储下标的单减栈。
正向遍历温度列表。对于温度列表中的每个元素 T[i],如果栈为空,则直接将 i 进栈,如果栈不为空,则比较栈顶元素 prevIndex 对应的温度 T[prevIndex] 和当前温度 T[i],如果 T[i] > T[prevIndex],则将 prevIndex 移除,并将 prevIndex 对应的等待天数赋为 i - prevIndex,重复上述操作直到栈为空或者栈顶元素对应的温度小于等于当前温度,然后将 i 进栈。
代码:
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
n=len(T)
ans=[0]*n
stack=[]
for i in range(n):
while stack and T[i]>T[stack[-1]]:
pre=stack.pop()
ans[pre]=i-pre
stack.append(i)
return ans
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
n=len(T)
ans=[0]*n
stack=[]
for i in range(n-1,-1,-1):
while stack and T[i]>=T[stack[-1]]:
stack.pop()
if stack:ans[i]=stack[-1]-i
stack.append(i)
return ans
LeetCode传送门
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例:
思路:
代码:
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n=len(heights)
left,right=[0]*n,[n]*n
mono_stack=[]
for i in range(n):
while mono_stack and heights[mono_stack[-1]]>=heights[i]:
right[mono_stack[-1]]=i
mono_stack.pop()
left[i]=mono_stack[-1] if mono_stack else -1
mono_stack.append(i)
ans=max((right[i]-left[i]-1)*heights[i] for i in range(n)) if n>0 else 0
return ans
LeetCode传送门
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
思路: 使用单调递减栈
代码:
class Solution:
def trap(self, height: List[int]) -> int:
n=len(height)
if n<3:return 0
ans,idx=0,0
stack=[]
while idx<n:
while stack and height[idx]>height[stack[-1]]:
top=stack.pop()
if not stack:
break
h=min(height[stack[-1]],height[idx])-height[top]
dist=idx-stack[-1]-1
ans+=dist*h
stack.append(idx)
idx+=1
return ans
LeetCode传送门
给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
思路:
由于 num1 是 nums2 的子集,可以忽略数组 nums1,先对将 nums2 中的每一个元素,求出其下一个更大的元素。随后对于将这些答案放入哈希映射(HashMap)中,再遍历数组 nums1,并直接找出答案。对于 nums2,我们可以使用单调递减栈来解决这个问题。
代码:
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
stack=[]
dic={
}
for i in range(len(nums2)):
while stack and nums2[stack[-1]]<nums2[i]:
dic[nums2[stack[-1]]]=nums2[i]
stack.pop()
stack.append(i)
ans=[-1]*len(nums1)
for i in range(len(nums1)):
ans[i]=dic.get(nums1[i],-1)
return ans
LeetCode传送门
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
思路:
可以维护一个单减栈寻找元素右边第一个比它大的元素。由于这道题的数组是循环数组,因此我们需要将每个元素都入栈两次。这样可能会有元素出栈找过一次,即得到了超过一个“下一个更大元素”,我们只需要保留第一个出栈的结果即可。
代码:
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
if not nums:return []
n=len(nums)
nums=nums*2
result=[-1]*n
stack=[0]
for i in range(1,2*n):
while stack and nums[stack[-1]]<nums[i]:
result[stack[-1]%n]=nums[i]
stack.pop()
stack.append(i%n)
return result
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
nums=nums*2
stack=[]
ans=[-1]*len(nums)
for idx,num in enumerate(nums):
while stack and nums[stack[-1]]<num:
ans[stack.pop()]=num
stack.append(idx)
return ans[:len(nums)//2]
LeetCode传送门
编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。
今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。
思路:
求元素左边第一个比它大的元素位置。构造一个单减栈。
代码:
class StockSpanner:
def __init__(self):
self.stack=[]
def next(self, price: int) -> int:
weight=1
while self.stack and self.stack[-1][0]<=price:
weight+=self.stack.pop()[1]
self.stack.append((price,weight))
return weight
单调队列:队列中元素之间的关系具有单调性,而且队首和队尾都可以进行出队操作,只有队尾可以进行入队操作。
队尾入队的时候维护单调性:
若队列有大小限制,则每次插入新元素的时候,需要从队头开始弹出元素,直到队列至少有一个空间留给当前元素。
举例来说, n u m s = [ 3 , 2 , 8 , 4 , 5 , 7 , 6 , 4 ] nums=[3,2,8,4,5,7,6,4] nums=[3,2,8,4,5,7,6,4],初始时, d e q u e = [ ] deque=[] deque=[],限制队列长度不能超过3,维护一个单增队列,
LeetCode传送门
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
思路:
窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题。遍历数组时,每轮保证:
代码:
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if not nums or k==0:return []
deque=collections.deque()
for i in range(k): # 未形成窗口
while deque and deque[-1]<nums[i]:
deque.pop()
deque.append(nums[i])
ans=[deque[0]]
for i in range(k,len(nums)): # 形成窗口后
if deque[0]==nums[i-k]:
deque.popleft()
while deque and deque[-1]<nums[i]:
deque.pop()
deque.append(nums[i])
ans.append(deque[0])
return ans
综上所述,单调队列实际上是单调栈的的升级版。单调栈只支持访问尾部,而单调队列两端都可以。
LeetCode题解
单调队列和单调栈详解
单调队列