对于一维数组而言,要寻找任意一个右边或者左边第一个比自己大或者小的元素的位置,就可以考虑使用单调栈
单调栈的作用是以空间换时间:因为在遍历的过程中需要用一个栈来记录我们遍历过的元素,单调栈的本质其实还是一个栈,只不过我们用来保存我们遍历过的元素的时候对栈中保存的元素有个要求,数字元素/字母元素是按从小到大
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:下一个更大的元素:简单题
给你两个没有重复元素的数组
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]:
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:中等题 详情点击链接看原题
给定一个循环数组
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
的长度取余就好
附上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
循环抛出栈中遇到'('
之前的字符,append
到tmp
数组中,相当于反转字符串(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
为[
时,将当前multi
和res
入栈,并分别置空置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
,直接入栈保存起来
注: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
Leetcode84:柱状图中的最大矩形:困难题
给你一个整数数组
nums
。每次 move 操作将会选择任意一个满足0 <= i < nums.length
的下标i
,并将nums[i]
递增1
。
返回使nums
中的每个值都变成唯一的所需要的最少操作次数
本文帮大家总结了面试中跟单调栈相关的高频考点,希望能帮助到大家,如果你觉得对你有用的话,赶紧点赞收藏吧~
算法题没有捷径,只有多刷+理解+调试,加油~