单调栈与单调队列算法详解及LeetCode经典题目(Python)

单调栈

单调栈:栈内的元素按照某种方式排序下单调递增或单调递减,如果新入栈的元素破坏的单调性,就弹出栈内元素,直到满足单调性。

单调栈分为单调递增栈和单调递减栈:

  • 单调递增栈:栈中数据出栈的序列为单调递减序列;
  • 单调递减栈:栈中数据出栈的序列为单调递增序列。

维护单调栈

维护单调递增栈:

  • 遍历数组中每一个元素,执行入栈:每次入栈前先检验栈顶元素和进栈元素的大小。
  • 如果栈空或进栈元素大于栈顶元素则直接入栈;如果进栈元素小于等于栈顶元素,则出栈,直至进栈元素大于栈顶元素。
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()

单调栈的作用

  • O ( n ) O(n) O(n)时间复杂度求出某个数的左边或右边第一个比它大或小的元素。

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=[]

  • i = 1 i=1 i=1:栈空,左边没有比它小的元素,故 L [ 1 ] = 0 L[1]=0 L[1]=0,同时下标 1 入栈, s t a c k = [ 1 ] stack=[1] stack=[1]
  • i = 2 i=2 i=2:当前元素 4 小于栈顶元素对应的元素 5,故将 1 弹出栈,此时栈空,故 L [ 2 ] = 0 L[2]=0 L[2]=0,然后将元素 4 对应的位置下标 2 入栈, s t a c k = [ 2 ] stack=[2] stack=[2]
  • i = 3 i=3 i=3:当前元素 3 小于栈顶元素对应的元素 4,故将 2 弹出栈,此时栈空,故 L [ 3 ] = 0 L[3]=0 L[3]=0,然后将元素 3 对应的位置下标 3 入栈, s t a c k = [ 3 ] stack=[3] stack=[3]
  • i = 4 i=4 i=4:当前元素 4 大于栈顶元素对应的元素 3,故 L [ 4 ] = s t a c k . t o p ( ) = 3 L[4]=stack.top()=3 L[4]=stack.top()=3,同时将元素 4 对应的下标 4 入栈, s t a c k = [ 3 , 4 ] stack=[3,4] stack=[3,4]
  • i = 5 i=5 i=5:当前元素 5 大于栈顶元素对应的元素 4,故 L [ 5 ] = s t a c k . t o p ( ) = 4 L[5]=stack.top()=4 L[5]=stack.top()=4,同时将元素 5 对应的下标 5 入栈, s t a c k = [ 3 , 4 , 5 ] stack=[3,4,5] stack=[3,4,5]

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=[]

  • i = 1 i=1 i=1:栈空,下标1 入栈, s t a c k = [ 1 ] stack=[1] stack=[1]
  • i = 2 i=2 i=2:当前元素 4 小于栈顶元素对应的元素 5,将 1 弹出栈,并且当前元素是栈顶元素右边第一个比其小的元素,故 R [ 1 ] = 2 R[1]=2 R[1]=2,将 2 入栈, s t a c k = [ 2 ] stack=[2] stack=[2]
  • i = 3 i=3 i=3:当前元素 3 小于栈顶元素对应的元素 4,将 2 弹出栈,并且当前元素是栈顶元素右边第一个比其小的元素,故 R [ 2 ] = 3 R[2]=3 R[2]=3,将 3 入栈, s t a c k = [ 3 ] stack=[3] stack=[3]
  • i = 4 i=4 i=4:当前元素 4 大于栈顶元素对应的元素 3 ,将 4 入栈, s t a c k = [ 3 , 4 ] stack=[3,4] stack=[3,4]
  • i = 5 i=5 i=5:当前元素 5 大于栈顶元素对应的元素 4 ,将 5 入栈, s t a c k = [ 3 , 4 , 5 ] stack=[3,4,5] stack=[3,4,5]
  • 没有被赋值的元素右边没有比其小的元素, R [ i ] = 0 R[i]=0 R[i]=0

4. 求第 i i i 个数右边第一个比它大的元素的位置

  • 从右到左遍历元素构造单减栈:一个元素右边第一个比它大的数的位置就是将它插入单减栈时栈顶元素的值,若栈为空,则说明不存在这么一个数。
  • 从左到右遍历元素构造单减栈:一个元素右边第一个比它大的数的位置就是将它弹出栈时即将入栈的元素的下标,如果没被弹出栈,说明不存在这么一个数。

当然,你也可以构造一个单增栈同时维护元素左边和右边第一个比它小的元素,或者,构造一个单减栈同时维护元素左边和右边第一个比它大的元素。

算法应用

739.每日温度【中等】

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

84.柱状图中最大的矩形【困难】

LeetCode传送门
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例:

  • 输入: [2,1,5,6,2,3]
  • 输出: 10

单调栈与单调队列算法详解及LeetCode经典题目(Python)_第1张图片单调栈与单调队列算法详解及LeetCode经典题目(Python)_第2张图片

思路:

  • 枚举某一根柱子 i i i 的高度作为矩形的高 h = h e i g h t [ i ] h=height[i] h=height[i]
  • 为获取最大矩形的宽,需要向左右两边扩展,具体地,需要找到左右两侧最近的高度小于 h h h 的柱子,显然可用单调栈解决。
  • 需要注意的是,如果左边没有比当前高度小的,则左边可以扩展到-1,如果右边没有比当前高度小的,则右边可以扩展到n。

代码:

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

42.接雨水【困难】

LeetCode传送门
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

单调栈与单调队列算法详解及LeetCode经典题目(Python)_第3张图片
示例:

  • 输入: [0,1,0,2,1,0,1,3,2,1,2,1]
  • 输出: 6

思路: 使用单调递减栈

  • 当后面的柱子高度比前面的低时,是无法接雨水的,但是,如果在这之后能找到更高的柱子,就能接到雨水,因此先入栈把它保存起来;
  • 当找到一根比前面高的柱子,就可以计算接到雨水:雨水区域的右边 r r r 指的是当前索引 i i i;底部的高度是栈顶 s t a c k . t o p ( ) stack.top() stack.top() 的高度,因为遇到了更高的右边,所以它即将出栈,使用 c u r cur cur 来记录它,并让它出栈;雨水区域的左边 l l l 就是新的栈顶 s t a c k . t o p ( ) stack.top() stack.top();。

代码:

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

496.下一个更大元素1【简单】

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

503.下一个更大元素2【中等】

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]

901.股票价格跨度【中等】

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

单调队列

单调队列:队列中元素之间的关系具有单调性,而且队首和队尾都可以进行出队操作,只有队尾可以进行入队操作。

维护单调队列

队尾入队的时候维护单调性:

  • 对于单调递增队列,设当前准备入队的元素为e,从队尾开始把队列中的元素逐个与e对比,把比e大或者与e相等的元素逐个删除,直到遇到一个比e小的元素或者队列为空为止,然后把当前元素e插入到队尾。
  • 对于单调递减队列也是同样道理,只不过从队尾删除的是比e小或者与e相等的元素。

若队列有大小限制,则每次插入新元素的时候,需要从队头开始弹出元素,直到队列至少有一个空间留给当前元素。

举例来说, 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,维护一个单增队列,

  • 3入队: d e q u e = [ 3 ] deque=[3] deque=[3]
  • 3从队尾出队,2入队: d e q u e = [ 2 ] deque=[2] deque=[2]
  • 8入队: d e q u e = [ 2 , 8 ] deque=[2,8] deque=[2,8]
  • 8从队尾出队,4入队: d e q u e = [ 2 , 4 ] deque=[2,4] deque=[2,4]
  • 5入队: d e q u e = [ 2 , 4 , 5 ] deque=[2,4,5] deque=[2,4,5]
  • 2从队头出队,7入队: d e q u e = [ 4 , 5 , 7 ] deque=[4,5,7] deque=[4,5,7]
  • 7从队尾出队,6入队: d e q u e = [ 4 , 5 , 6 ] deque=[4,5,6] deque=[4,5,6]
  • 6从队尾出队,5从队尾出队,4从队尾出队,4入队: d e q u e = [ 4 ] deque=[4] deque=[4]

单调队列的作用

  • 区间最小(最大)值问题。
  • 优化动态规划:eg优化多重背包

算法应用

239.滑动窗口最大值【困难】

LeetCode传送门
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

思路:
窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题。遍历数组时,每轮保证:

  • deque 内 仅包含窗口内的元素 ⇒ \Rightarrow 每轮窗口滑动移除了元素 nums[i - 1]] ,需将 deque 内的对应元素一起删除。
  • deque 内的元素 非严格递减 ⇒ \Rightarrow 每轮窗口滑动添加了元素 nums[j + 1] ,需将 deque 内所有

代码:

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

单调栈和单调队列的区别和联系

相同点

  • 单调队列和单调栈的“头部”都是最先添加的元素,“尾部”都是最后添加的元素。
  • 递增和递减的判断依据是:从栈底(队尾)到栈顶(队首),元素大小的变化情况。队列和栈是相反的。
  • 操作非常相似。当队列长度为无穷大时,递增的单调队列和递减的单调栈,排列是一样的!这是因为,长度为无穷大的的队列不会在“头部”有出队操作,而在“尾部”的操作是一模一样的:数据都从“尾部”进入,并按照相同的规则进行比较。
  • 两者维护的时间复杂度都是O(n),因为每个元素都只操作一次。

区别

  • 队列可以从队列头弹出元素,可以方便地根据入队的时间顺序(访问的顺序)删除元素。这样导致了单调队列和单调栈维护的区间不同。当访问到第i个元素时,单调栈维护的区间为[0, i),而单调队列维护的区间为(lastpop, i)
  • 单调队列可以访问“头部”和“尾部”,而单调栈只能访问栈顶(也就是“尾部”)。这导致单调栈无法获取[0, i)的区间最大值/最小值。

综上所述,单调队列实际上是单调栈的的升级版。单调栈只支持访问尾部,而单调队列两端都可以。

参考

LeetCode题解
单调队列和单调栈详解
单调队列

你可能感兴趣的:(数据结构)