Leetcode刷题笔记—单调栈篇

Leetcode刷题笔记—单调栈篇

一、单调栈的相关概念

对于一维数组而言,要寻找任意一个右边或者左边第一个比自己大或者小的元素的位置,就可以考虑使用单调栈

单调栈的作用是以空间换时间:因为在遍历的过程中需要用一个栈来记录我们遍历过的元素,单调栈的本质其实还是一个栈,只不过我们用来保存我们遍历过的元素的时候对栈中保存的元素有个要求,数字元素/字母元素是按从小到大 or 从大到小的顺序来存储?

在使用单调栈的时候需要明确的几点:
1.单调栈里存放的元素是什么,单调栈里面存放的元素是中间过程还是结果集 ?

我们一般用单调栈中存放我们遍历过的元素的下标,也可以用来保存我们遍历过的元素本身,具体问题具体分析

2.单调栈里的元素是递增的还是递减的呢 ?

要搞清楚递增还是递减首先得明确的是方向
这里我们按从栈头—>栈底的顺序(这个没有明确规定,完全取决于你自己),元素从小到大则为单调递增栈,反之则为单调递减栈

3.入栈出栈的时机 ?
对于第一题每日温度和第二题下一个更大的元素我们要找的是右边第一个比自己大的元素,所以在遍历过程中,入栈的肯定是比栈顶元素小的元素

二、单调栈的相关题型

第一题:每日温度

Leetcode739:每日温度中等题 详情请点击链接看原题

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替

本题要求找到右边第一个比自己大的元素,在遍历数组的时候我们是无法知道遍历某个元素的时候其实是不是之前遍历过更小的或者更大的,所以我们使用一个容器(这里使用单调栈)来记录我们遍历过的元素
使用单调栈的三个判断条件

case1:当前遍历的元素T[i] 小于 栈顶元素T[stack.top()]的情况
case2:当前遍历的元素T[i] 等于 栈顶元素T[stack.top()]的情况
case3:当前遍历的元素T[i] 大于 栈顶元素T[stack.top()]的情况

附上python题解完整代码

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        stack = []  # 单调递增栈(按栈头——>栈底)
        answer = [0] * len(temperatures)  # 用来保存结果集(即比当前元素大的下一个元素相隔的天数)
        stack.append(0)	# 将第一个元素下标 0 入栈

        for i in range(1, len(temperatures)):
            if temperatures[i] <= temperatures[stack[-1]]:  # case1 和 case2
                stack.append(i)  # 如果遍历的元素小于栈顶元素则将元素下标入栈
            else:	# case3
                while stack and temperatures[i] > temperatures[stack[-1]]:
                    answer[stack[-1]] = i - stack[-1]	# 栈顶元素的右边的最大的元素就是当前遍历的元素
                    stack.pop()	# 弹出栈顶元素
                stack.append(i)
        return answer

注意:

answer 直接初始化为 0,如果 answer 没有被更新,说明这个元素的右边没有比它更大的了
answer[stack[-1]] = i - stack[-1] # 以单调栈stack中的栈顶元素为准来保存结果集,记录栈顶元素右边离他最近且比他大的第一个元素与它相隔的天数

一图胜千言(附上大佬做的题解动画

第二题: 下一个更大的元素

Leetcode496:下一个更大的元素:简单题

给你两个没有重复元素的数组 nums1nums2 ,其中nums1nums2 的子集,请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值
nums1 中数字 x 的下一个更大元素是指 xnums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1

1. 结果集 result 数组定义为多大 ?
要求nums1的每个元素在nums2中下一个比当前元素大的元素,故应以nums1为准,定义一个大小和nums1相同的结果集数组

2. 结果集 result 数组应该初始化为多少呢 ?
题目说如果不存在对应位置就输出 -1, 所以result数组如果某位置没有被赋值,那么就应该是 -1,所以应该初始化为-1
3. 栈中保存的元素是什么 ?
我们要求的是nums1中的某个元素在nums2中的下一个更大的元素,所以栈中保存的应该是nums2中的元素,

step1: 先将nums2的第一个元素入栈(栈顶元素),
step2:遍历nums2中剩余元素,遍历到比栈顶元素小的元素将新元素入栈(作为新的栈顶元素)
step3: 遍历到比栈顶元素大的元素则需要判断栈顶元素nums1中的位置
step4: 确定好位置即找到了nums1中该位置上元素的下一个更大的元素,加入到结果集result

附上python题解完整代码

class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        ans = [-1] * len(nums1)
        stack = []
        for num in range(0, len(nums2)):
            while stack and nums2[stack[-1]] < nums2[num]:
                if nums2[stack[-1]] in nums1:
                    index = nums1.index(nums2[stack[-1]])
                    ans[index] = nums2[num]
                stack.pop()
            stack.append(num)
        return ans

第三题: 下一个更大的元素 II

Leetcode503:下一个更大的元素 II:中等题 详情点击链接看原题

给定一个循环数组 numsnums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。
数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1

本题相较于上一题, 其实就是 nums2 = nums1 = [1, 5, 3, 2, 6, 4, 0],在上一题中的result = [5, 6, 6, 6, -1, -1, -1],但在循环数组中,最后的两个元素 40 是可以找到下一个更大的元素的

方法1
我们看到这道题的第一反应一般是我直接把两个数组拼接在一起,然后使用单调栈求下一个最大值不就行了!

方法2解题思路
相较于上一题,我们只需遍历两次nums数组即可确定循环数组中的每个元素的下一个更大的元素
与上一题的区别除以下两点外,其余思路与上一题完全一致

1.遍历过程中stack作为辅助栈,保存的是nums中元素的下标
2.注意对于超出nums长度方位的次序,对nums的长度取模就好
3.stack中保存的是 nums 中元素的下标,此时还需要一个result数组保存最终结果集

附上python题解完整代码

class Solution:
    def nextGreaterElements(self, nums: List[int]) -> List[int]:
        nums_size = len(nums)
        ans = [-1] * nums_size
        i = 0
        stack = []
        while i < 2 * nums_size:
            temp = i % nums_size
            while stack and nums[stack[-1]] < nums[temp]:
                index = stack.pop()
                ans[index] = nums[temp]
            stack.append(temp)
            i += 1
        return ans

第四题: 链表中的下一个更大节点

Leetcode1019. 链表中的下一个更大节点:中等题 详情点击链接看原题

给定一个长度为 n 的链表 head
对于列表中的每个节点,查找下一个 更大节点 的值。也就是说,对于每个节点,找到它旁边的第一个节点的值,这个节点的值 严格大于 它的值

python代码解法

class Solution:
    def nextLargerNodes(self, head: Optional[ListNode]) -> List[int]:
        link_len = 0
        p1 = head
        while p1:  # 统计链表长度
            p1 = p1.next
            link_len += 1
            
        ans = [0] * link_len
        stack = []
        p2 = head
        i = 0
        while p2:
            while stack and p2.val > stack[-1][1].val:
                ans[stack[-1][0]] = p2.val
                stack.pop()
            stack.append((i, p2))
            i += 1
            p2 = p2.next
        return ans

第五题: 移掉 K 位数字

Leetcode402:移掉 K 位数字:中等题 详情点击链接看原题

给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字

python代码解法

class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        if len(num) == k:  # 如要移除的元素数量等于串的长度即移除所有元素,直接返回'0'
            return '0'
        stack = []  # 单调递减栈,保存 num 中从高位到低位遍历过的元素
        for i in num:
            while k > 0 and stack and i < stack[-1]:
                stack.pop()	 # 处于高位还比别人大,最先移除的就是你
                k -= 1
            if i != '0' or stack:	 # 若当前扫描的元素为0但栈不为空说明此时的0非前导0则入栈
                stack.append(i)

        while k > 0 and stack:	# 遍历结束时,有可能还没删够 k 个字符
            stack.pop()
            k -= 1

        if not stack:	# 如果栈已经空了返回'0'
            return '0'

       return ''.join(stack)

注意1:我们首先要明确的是我们用单调栈保存的元素是什么,对num从高位到低位遍历,处于高位的并且还大的数字(不干掉你干谁)

num的从头(高位)到尾(低位)的遍历过程中,我们不知道遍历过程中的哪个元素更大一些,故我们使用栈来保存我们遍历过的元素,与上一题不同的是在这一题中单调栈里面存放的是我们遍历的元素(上题中栈只是用来辅助我们得到最终的结果集并且里面保存的还是下标)

注意2: 题目要求输出不能含前导 0

我们需要不让前导 0入栈,在栈为空且当前字符为 0 的前提下不让入栈,那么取反(当前元素不为0或者栈不为空时)就让入栈

注意3: 遍历结束时,有可能还没删够 k 个字符

在前面一轮的遍历num过程中,由于当前遍历的元素大于栈顶元素则入栈,遍历过程中的低位元素大于栈顶的高位元素就将低位元素入栈,遍历完后栈头(数值大的低位元素)—>栈底(数值小的高位元素),故此时栈中元素从栈头—>栈底的顺序是个单调递减栈,我们应该删除栈顶的低位元素才能保证最终数字最小

逆向思维: 移除 k 位数字反过来就是保留 n - k位数字
python代码解法

class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        if k == len(num):
            return '0'

        stack = []
        remain = len(num) - k
        for i in num:
            while stack and stack[-1] > i and k > 0:
                stack.pop()
                k -= 1

            stack.append(i)
        return "".join(stack[:remain]).lstrip('0') or '0'

第六题:去除重复字母

Leetcode316:去除重复字母:中等题

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)

分析
这道题相较于上两道题难了一点,该题我们需要借助于哈希表(字典)来统计我们元素以及元素出现的次数
该题中栈用来保存结果集,要使返回结果的字典序最小,(故先将第一个元素入栈作为栈顶元素),如果遍历中的某个元素大于栈顶元素则入栈,否则栈顶元素出栈,新元素入栈为栈顶元素

1.建立一个字典,key 为对应的元素,value为元素出现的次数
2.从左往右遍历字符串,每遍历一个字符,其对应的出现次数 value - 1
3.对于每个字符,如果出现次数大于1,我们是否丢弃还是保留取决于栈中相邻的字典序谁更大,如果栈中相邻的元素字典序更大,那么我们选择丢弃相邻的栈中的元素

附上python题解完整代码

class Solution:
    def removeDuplicateLetters(self, s: str) -> str:
        stack = []
        remains = Counter(s)    # 统计 s 中每个元素以及每个元素出现的次数
        for i in s:
            if i not in stack:
                while stack and remains[stack[-1]] > 0 and stack[-1] > i:
                    stack.pop()
                stack.append(i)
            remains[i] -= 1
        return "".join(stack)

第七题:接雨水

Leetcode42:接雨水:困难题 详情点击链接看原题

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

注1:单调栈元素的顺序 ?
从栈头到栈底的顺序应该是从小到大的顺序
一旦发现添加的柱子高度大于栈顶元素就表示此时出现凹槽了,栈顶元素即凹槽底部,栈顶元素的第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子

注2: 遇到高度相同的柱子怎么办 ?
遇到相同的元素更新栈内下标,将栈顶元素弹出,将新元素入栈(因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度)

注3: 栈里要保存什么数值 ?
通过 长 * 宽 来计算雨水面积,长为柱子的高度,宽为柱子之间的下标
栈中有没有必要保存柱子的高度和下标两种数据呢,栈中存放下标就行,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了

注4: 雨水面积的计算?
通过三个元素来接水,栈顶,栈顶的下一个元素,以及即将入栈的元素
雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度
雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度)

python完整题解代码

class Solution:
    def trap(self, height: List[int]) -> int:
        stack = [0]
        result = 0
        for i in range(1, len(height)):
            if height[i] < height[stack[-1]]:
                stack.append(i)

            # 当前的柱子高度和栈顶一致时,左边的第一个是不可能存放雨水的
            elif height[i] == height[stack[-1]]:
                stack.pop()
                stack.append(i)

            else:
                while stack and height[i] > height[stack[-1]]:
                    mid_height = height[stack[-1]]
                    stack.pop()
                    if stack:
                        right_height = height[i]
                        left_height = height[stack[-1]]
                        h = min(right_height, left_height) - mid_height  # 两侧的较矮一方的高度 - 凹槽底部高度
                        w = i - stack[-1] - 1   # 凹槽右侧下标 - 凹槽左侧下标 - 1: 只求中间宽度
                        result += h * w
                stack.append(i)
        return result

第八题: 车队

Leetcode853. 车队:中等题 详情点击链接看原题

在一条单行道上,有 n 辆车开往同一目的地。目的地是几英里以外的 target

解题思路:
车手 A,B 之间只存在两种情况
posA < posBA 率先到达终点,那么 A 势必在某个时刻追上 B, A,B 在该时刻形成车队
posA < posBB 率先到达终点,那么 A 始终追不上BA、B各自成车队

python代码解法

class Solution:
    def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
        n = len(position)
        time_record = [0] * n     # 用来记录哪个车队到达终点的时间
        car_queue = sorted(zip(position, speed))
        for i, (pos, sp) in enumerate(car_queue):
            time_record[i] = (target - pos) / sp

        stack = []
        for t in time_record:
            while stack and t >= stack[-1]:  # 如果起始位置在后面的车队到达终点的时间比后面的车队到达终点的时间要早,则必然能形成车队
                stack.pop()
            stack.append(t)
        return len(stack)    # 最后栈中元素个数即车队数

第九题:使数组唯一的最小增量

Leetcode945. 使数组唯一的最小增量 :中等题 详情点击链接看原题

给你一个整数数组 nums 。每次 move 操作将会选择任意一个满足 0 <= i < nums.length的下标 i,并将 nums[i] 递增 1
返回使 nums 中的每个值都变成唯一的所需要的最少操作次数

题目分析
数组中必然存在重复元素,不然没有分析的必要,要使得每个值唯一的最少 move 次数,我们首先应该对该数组进行排序,辅助栈的作用在这里用来保存我们 move 之后的元素

step1:先对nums 进行从小到大排序,将排序后的第一个元素入栈,遍历数组中的剩余元素
step2:如果遍历的元素等于栈顶元素,则说明数组中存在重复元素,要使元素唯一的最少增量我们可以+1或者-1,因为数组是从小到大排序的,所以我们这里的move次数为1
step3: 如果遍历到的元素小于栈顶元素,则需要判断二者相差多少,用相差距离+1即二者之间的最少move次数
step4:如果遍历的元素大于栈顶元素则无需move,直接入栈保存起来
注:step2step3的两步操作其实可以合并,这里为了方便大家理解不做简写

python完整题解代码

class Solution:
    def minIncrementForUnique(self, nums: List[int]) -> int:
        nums.sort()	 # 先排序 
        stack = [nums[0]]
        move_nums = 0
        for num in nums[1:]:
            if num == stack[-1]:  # 如果遍历到的元素和栈顶元素相等,则需要move一次使得栈中元素单调递增
                num += 1  # +1就是使得单调递增的最少增量
                stack.append(num)
                move_nums += 1
            elif num < stack[-1]:  # 如果遍历到的元素小于栈顶元素则判断二者相差多少,最后再加一个1(即最少增量)
                if stack[-1] - num >= 1:
                    min_increment = stack[-1] - num + 1  # 计算栈顶元素和遍历元素的相差距离,再+1即最少增量
                    move_nums += min_increment
                    num += min_increment
                    stack.append(num)
            elif num > stack[-1]:  # 本身就是单调递增(直接入栈)
                stack.append(num)
        return move_nums

第十题:柱状图中的最大矩形

Leetcode84:柱状图中的最大矩形:困难题

给你一个整数数组 nums 。每次 move 操作将会选择任意一个满足 0 <= i < nums.length 的下标 i,并将 nums[i] 递增 1
返回使 nums 中的每个值都变成唯一的所需要的最少操作次数

栈顶和栈顶的下一个元素以及要入栈的三个元素组成我们要求的最大面积的高度和宽度
python代码解法(单调栈解法)

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack = []
        max_area = 0
        heights = [0] + heights + [0]
        for i in range(len(heights)):
            # heights[i] 为当前正在遍历的元素
            # heights[stack[-1]] > heights[i]:当栈顶元素大于当前正在遍历的元素
            while stack and heights[stack[-1]] > heights[i]:
                height = heights[stack[-1]]   # 开始计算以栈顶元素为高度的矩形的最大面积
                stack.pop(-1)  # 弹出栈顶元素
                left_index = stack[-1]  # 当前栈顶元素即左边第一个比 height 小的位置
                right_index = i  # 当前正在遍历的元素即右边第一个比 height 小的位置
                max_area = max(max_area, (right_index - left_index - 1) * height)
            stack.append(i)  # 下标入栈
        return max_area


if __name__ == '__main__':
    s = Solution()
    heights = [2, 1, 5, 6, 2, 3]
    print(s.largestRectangleArea(heights))

三、单调队列

第一题:滑动窗口最大值

Leetcode239. 滑动窗口最大值:困难题

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位

解题思路
单调队列:关键在于如何维护队列里面元素的单调递增和单调递减,其实队列中没有必要维护窗口里的所有元素,只要维护有可能成为窗口里最大值的元素就可以了,同时保证从队首到队尾里面的元素数值是由大大小的

不要以为实现的单调队列就是对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别呢

python代码解法

from collections import deque


class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        queue = deque()
        left, right = 0, 0
        while right < k:   # 先形成大小为k的滑动窗口
            while queue and queue[-1] < nums[right]:
                queue.pop()
            queue.append(nums[right])
            right += 1
        res = [queue[0]]
        while right < len(nums):   # 形成窗口后
            if nums[right - k] == queue[0]:
                queue.popleft()
            while queue and queue[-1] < nums[right]:
                queue.pop()
            queue.append(nums[right])
            res.append(queue[0])
            right += 1
        return res

python代码解法二:设计单调队列数据结构

from collections import deque
from typing import List


class MyQueue:
    def __init__(self):
        self.queue = deque()

    def pop(self, value):
        if self.queue and value == self.queue[0]:
            self.queue.popleft()

    def push(self, value):
        while self.queue and value > self.queue[-1]:
            self.queue.pop()
        self.queue.append(value)


class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        q = MyQueue()
        ans = []
        for i in range(k):
            q.push(nums[i])
        ans.append(q.queue[0])
        for i in range(k, len(nums)):
            q.pop(nums[i - k])
            q.push(nums[i])
            ans.append(q.queue[0])
        return ans

总结

本文帮大家总结了面试中跟单调栈相关的高频考点,希望能帮助到大家,如果你觉得对你有用的话,赶紧点赞收藏吧~
算法题没有捷径,只有多刷+理解+调试,加油~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_lover_forever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值