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
中的栈顶元素为准来保存结果集,记录栈顶元素右边离他最近且比他大的第一个元素与它相隔的天数
一图胜千言(附上大佬做的题解动画)
第二题: 下一个更大的元素
给你两个没有重复元素的数组
nums1
和nums2
,其中nums1
是nums2
的子集,请你找出nums1
中每个元素在nums2
中的下一个比其大的值
nums1
中数字x
的下一个更大元素是指x
在nums2
中对应位置的右边的第一个比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:中等题 详情点击链接看原题
给定一个循环数组
nums
(nums[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]
,但在循环数组中,最后的两个元素 4
和 0
是可以找到下一个更大的元素的
方法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'
第六题:去除重复字母
给你一个字符串
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 < posB
且 A
率先到达终点,那么 A
势必在某个时刻追上 B
, A,B
在该时刻形成车队
posA < posB
且 B
率先到达终点,那么 A
始终追不上B
,A、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
,直接入栈保存起来
注:step2
和step3
的两步操作其实可以合并,这里为了方便大家理解不做简写
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
第十题:柱状图中的最大矩形
给你一个整数数组
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))
三、单调队列
第一题:滑动窗口最大值
给你一个整数数组
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
总结
本文帮大家总结了面试中跟单调栈相关的高频考点,希望能帮助到大家,如果你觉得对你有用的话,赶紧点赞收藏吧~
算法题没有捷径,只有多刷+理解+调试,加油~