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]:
        stack = [nums2[0]]      # 将 nums2 中的第一个元素入栈
        result = [-1] * len(nums1)

        for i in nums2[1:]:     # 遍历 nums2 中的剩余元素
            if i <= stack[-1]:
                stack.append(i)
            else:
                while stack and i > stack[-1]:
                    if stack[-1] in nums1:
                        index = nums1.index(stack[-1])  # 找到栈顶元素在 nums1 中的下标位置
                        result[index] = i       # 即可确定 nums1 中的某个元素的下一个更大的元素
                    stack.pop()	# 将找到结果的元素出栈
                stack.append(i)
        return result

第三题

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的长度取余就好

附上python题解完整代码

class Solution:
    def nextGreaterElements(self, nums: List[int]) -> List[int]:
        stack = [0]
        nums_len = len(nums)
        result = [-1] * nums_len

        for i in range(1, 2 * len(nums)):
            if nums[i % nums_len] <= nums[stack[-1]]:
                stack.append(i % nums_len)
            else:
                while stack and nums[i % nums_len] > nums[stack[-1]]:
                    result[stack[-1]] = nums[i % nums_len]
                    stack.pop()
                stack.append(i % nums_len)
        return result

第四题

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[0]]  # 单调递减栈,保存 num 中从高位到低位遍历过的元素
        for i in num[1:]:
            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([i for i in stack])

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

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

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

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

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

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

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

第五题

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

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

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)

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

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

第六题

Leetcode1190:反转每对括号间的子串:中等题

给出一个字符串 s: 仅含有小写英文字母和括号
请你按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果

方法1:解题思路

1.创建一个存放字符串的辅助栈 stack 以及一个保存当前字符串的变量 cur_str
2.遇到 ( 就将当前的字符串推入栈,并将当前字符串 cur_str 设置为空
3.遇到 ) 就将当前的字符串反转,然后与栈顶元素合并,并将栈顶元素弹出
4.遇到普通的字符就将其添加到当前字符串 cur_str 的尾部

python完整题解代码

class Solution:
    def reverseParentheses(self, s: str) -> str:
        stack = []
        cur_str = ""
        for c in s:
            if c == '(':
                stack.append(cur_str)
                cur_str = ""
            elif c == ')':
                cur_str = stack.pop() + cur_str[::-1]
            else:
                cur_str += c
        return cur_str

方法2解题思路

case1: 只要不是右括号')'就直接入栈
case2: 遇到了右括号')'

  • while循环抛出栈中遇到 '(' 之前的字符, appendtmp数组中,相当于反转字符串(temp 用来保存左括号和右括号中间的字符串)
  • 最后将栈中的左括号输出
    注:由于栈先进先出的特性,我们将s中的'('')'中间的字符先 push()stack中再pop()temp中,stack中的栈顶元素为temp中的栈底元素(即完成括号中间字符的翻转)

python完整题解代码

class Solution:
    def reverseParentheses(self, s: str) -> str:
        stack = []
        for c in s:
            if c == ')':	# case2
                temp = []		# temp作为辅助栈:暂存括号中间的字符
                while stack and stack[-1] != '(':
                    temp.append(stack.pop())
                if stack[-1] == '(':
                    stack.pop()
                stack.extend(temp)	# 将中间反转的结果集拼接到stack末尾
            else:		# case1
                stack.append(c)
        return "".join(stack)

第七题

Leetcode394:字符串解码:中等题
博主水平有限,剽窃大佬解题思路,总结K神大佬题解
本题的解题思路相当于上一题类似,相当于上一题的进阶版本,同时也是一道面试高频考题,大家注意多多上手调试
stack:作为辅助栈用来保存,倍数和中间结果
res:结果集

算法流程: 遍历字符串 s 中的每个字符 c
case1:当 c 为数字时,将数字字符转化为数字,用于后续的倍数计算
case2:当 c 为字母时,在res的尾部添加 c
case3:当c[时,将当前multires入栈,并分别置空置0
case4:当c]时,stack出栈,拼接字符串到结果集中

class Solution:
    def decodeString(self, s: str) -> str:
        stack, res, multi = [], "", 0	# 初始化
        for c in s:
            if c == '[':	# case3
                stack.append([multi, res])	# 先将之前的 multi 和 res 入栈
                res, multi = "", 0
            elif c == ']':		# case4
                cur_multi, last_res = stack.pop()
                res = last_res + cur_multi * res
            elif '0' <= c <= '9':	# case1
                multi = multi * 10 + int(c)            
            else:		# case2
                res += c
        return res

第八题

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

第九题

Leetcode945. 使数组唯一的最小增量 :中等题
接下来给大家分享的这道题是博主本人再某次大厂面试中碰到的一道原题,他的官方解法是贪心,因为手撕代码只有30分钟的时间,所以我用的是单调栈的解法来解的这道题

给你一个整数数组 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 中的每个值都变成唯一的所需要的最少操作次数

总结

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

你可能感兴趣的:(leetcode,笔记,面试,python)