20. 有效的括号 - 力扣(LeetCode)
栈做法:
class Solution:
def isValid(self, s: str) -> bool:
stack = [s[0]]
for i in range(1, len(s)):
if stack and (
s[i] == ')' and stack[-1] == '('
or s[i] == ']' and stack[-1] == '['
or s[i] == '}' and stack[-1] == '{'
):
stack.pop()
else: # 其余情况都append
stack.append(s[i])
if stack: return False # stack若非空,要么说明左括号没有遇到对应右括号导致未被消除,要么说明右括号没有对应左括号导致被append
else: return True
不断合并做法:
class Solution:
def isValid(self, s: str) -> bool:
s_len = len(s)
if s_len % 2 == 1: #长度为奇数则必无效
return False
while '{}' in s or '[]' in s or '()' in s:
s = s.replace('{}', '')
s = s.replace('[]', '')
s = s.replace('()', '')
return s == ''
155. 最小栈 - 力扣(LeetCode)
本题细究之下还是比较难的,O(1)时间O(n)额外空间可以用辅助栈,但若再要求O(1)额外空间呢?
本题难点在于实现O(1)时间获取栈内最小值(尤其是当push和pop之后栈内最小值可能有变动),以及如果更进阶的,需要同时实现O(1)额外空间。
思路为维护一个类似前缀和概念的前缀最小值preMin,每一个结点(或者说索引)的preMin都是当前结点及其之前结点的最小值。(可以写成下面的递推形式preMin[i+1] = min(preMin[i], val[i+1])
)
灵茶山艾府的解法(O(n)额外空间,其实就是辅助栈记录每一个结点的前缀和):
该解法中,栈中的每一个元素是一个元组,记录(当前结点值,当前结点值对应的preMin)
class MinStack:
def __init__(self):
# 这里的 0 写成任意数都可以,反正用不到
self.st = [(0, inf)] # 栈底哨兵
def push(self, val: int) -> None:
self.st.append((val, min(self.st[-1][1], val)))
def pop(self) -> None:
self.st.pop()
def top(self) -> int:
return self.st[-1][0]
def getMin(self) -> int:
return self.st[-1][1]
真正O(1)额外空间的解法:
重点在于,要用栈记录一路以来的差值:当前结点值 - 前一结点对应的前缀最小值preMin
,而不是用栈记录当前结点值!然后再用额外的一个变量preMin来维护当前的前缀最小值(仅在差值为负数时更新)。
这样的做法用O(1)额外空间和原本必要的O(n)栈空间,变相记录了每一处结点的preMin和值!
class MinStack:
def __init__(self):
# diffStack数据栈,但是存储的是当前结点减去前一个结点的(也就是未更新的)preMin的差值!
# 这样当push时,先计算当前差值并保存,如果差值为负说明当前结点的(也就是新preMin)是由当前结点提供的,需要更新preMin;
# 当pop时,由于diffStack保存的是当前结点与前一个结点的preMin的差值,所以如果栈顶元素对应差值为负,说明栈顶元素push时修改了preMin,我们可以根据栈顶元素差值和栈顶元素对应的preMin恢复到前一个元素的preMin!然后再pop掉栈顶元素即可。
self.diffStack = [] #初始化为空
self.preMin = inf # 记录的是当前最小值(前缀最小值),初始化为无穷大可以在push第一个元素时简化判断
def push(self, val: int) -> None:
diff = val - self.preMin # 获取当前结点值 与 前一结点对应preMin 的差值
self.diffStack.append(diff) # append操作本身就是O(1)时间
if diff < 0: # diff小于0时,说明当前结点值更小,需要修改preMin
self.preMin = val # 更新preMin
def pop(self) -> None:
diff = self.diffStack.pop() # pop栈顶的diff元素
if diff < 0: # 差值为负时,说明当前结点(也就是未经过pop的栈顶)修改了前一个结点处的preMin,需要恢复preMin为之前更大的preMin
self.preMin -= diff
def top(self) -> int:
top_diff = self.diffStack[-1]
if top_diff < 0: # 注意这里也要判断,如果栈顶元素为负数,说明在栈顶元素处更新了preMin,且更新的preMin必然等于当前栈顶元素对应的真正的结点值!此时是不能用差值+preMin来算的
return self.preMin
return top_diff + self.preMin
def getMin(self) -> int:
return self.preMin
题目下方评论区用链表的实现法(比较巧妙,但并不是O(1)额外空间):
class Node:
def __init__(self, val, min_, next_):
self.val = val
self.min = min_
self.next = next_
class MinStack:
def __init__(self):
self.head = Node(0, float('inf'), None)
def push(self, val: int) -> None:
self.head = Node(val, min(self.getMin(), val), self.head)
def pop(self) -> None:
self.head = self.head.next
def top(self) -> int:
return self.head.val
def getMin(self) -> int:
return self.head.min
394. 字符串解码 - 力扣(LeetCode)
递归写法比较直观好写,但是记得本题要考虑的细节比较多!(尤其是k可能是100这种多位的数字,而不一定是一位数字)
class Solution:
def decodeString(self, s: str) -> str:
def getSubString(s, start):
# 递归写法,从start索引处开始(保证start处必为字母或数字),向后搜索,扩充子串;
# 如果遇到右括号,立刻返回子串和右括号右边格子的索引newStart。
# 如果遇到字母,直接并入子串;
# 如果遇到数字,从下下个索引(必为字母或数字)开始递归getSubString,将递归结果并入子串并继续向后搜索直到遇见第一个右括号
i = start
subs = "" # 子串
while i < len(s) and s[i] != ']':
char = s[i] # 当前字符
if char.isalpha(): # 当前字符是字母
subs += char
i += 1
elif char.isdigit(): # 当前字符是数字
# 注意这里k不一定是只有一位!可能有连续几位数字,所以要获取完整的数字
while s[i+1].isdigit():
char += s[i+1]
i += 1
temps, i = getSubString(s, i + 2) # 从数字的下下个字符开始递归,返回递归获取到的子串,此处同时还已经在递归后更新索引i防止重复'访问
subs += int(char) * temps
return subs, i + 1
# 注意getSubString无法处理类似"2[ab]cd3[ef]gh"中最外层括号外的末尾字母,如cd和gh,因为到ab的右括号就直接返回了,所以要额外处理末尾的纯字符!
# i = 0
# subs = ""
# while i < len(s):
# newsubs, newStart = getSubString(s, i)
# subs += newsubs
# i = newStart
# 也可以直接在整个s外层套一个"1[]"变成1[s],就可以直接传入不用考虑末尾纯字符了
subs, _ = getSubString("1[" + s + "]", 0)
return subs
学习k神题解:
递归法和我思路基本一致,代码如下,比我的简洁,对连续数字字符、括号外末尾字符的处理也比我的好:
class Solution:
def decodeString(self, s: str) -> str:
def dfs(s, i):
res, multi = "", 0 # multi对应着k
while i < len(s):
if '0' <= s[i] <= '9':
multi = multi * 10 + int(s[i])
elif s[i] == '[':
i, tmp = dfs(s, i + 1)
res += multi * tmp
multi = 0 # multi用完之后要清零!
elif s[i] == ']':
return i, res
else:
res += s[i]
i += 1
return res
return dfs(s,0)
辅助栈法:
算法流程:
构建辅助栈 stack, 遍历字符串 s 中每个字符 c;
当 c 为数字时,将数字字符转化为数字 multi,用于后续倍数计算;
当 c 为字母时,在 res 尾部添加 c;(这一步能够保证可以读到最外层括号外部末尾的字母了!)
当 c 为 [ 时,将当前 multi 和 res 入栈,并分别置空置 0:
- 记录此 [ 前的临时结果 res 至栈,用于发现对应 ] 后的拼接操作;
- 记录此 [ 前的倍数 multi 至栈,用于发现对应 ] 后,获取 multi × [...] 字符串。
- 进入到新 [ 后,res 和 multi 重新记录。
当 c 为 ] 时,stack 出栈,拼接字符串 res = last_res + cur_multi * res,其中:
- last_res是上个 [ 到当前 [ 的字符串,例如 "3[a2[c]]" 中的 a;
- cur_multi是当前 [ 到 ] 内字符串的重复倍数,例如 "3[a2[c]]" 中的 2。
- 返回字符串 res。
class Solution:
def decodeString(self, s: str) -> str:
stack, res, multi = [], "", 0
for c in s:
if c == '[':
stack.append([multi, res]) # 发现新的左括号,那么之前的multi和res都要入栈保存,直到发现对应的右括号,那时pop出来的multi和res分别对应右边子串的重复次数cur_multi和左边已有字串last_res
res, multi = "", 0 # 在递归函数中记录新的res和multi
elif c == ']':
cur_multi, last_res = stack.pop()
res = last_res + cur_multi * res
elif '0' <= c <= '9':
multi = multi * 10 + int(c)
else:
res += c
return res
739. 每日温度 - 力扣(LeetCode)
经典的单调栈题目
本题从左向右遍历,单调栈内的元素都是索引,从栈底到栈顶对应的温度总是保持呈递减趋势,遇到比栈顶元素温度高的待入栈元素,就不断pop直到待入栈元素能够在保证栈单调递减的前提下入栈
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
# 显然是单调栈解决
stack = [0] # 单调栈内的元素都是索引,从栈底到栈顶呈递减趋势
n = len(temperatures)
res = [0] * n # 提前开好结果空间
for i in range(1, n):
t = temperatures[i] # 当前待入栈的温度
while stack and temperatures[stack[-1]] < t: # 出栈直到栈顶元素对应温度高于当前温度或栈为空
top = stack.pop() # 栈顶出栈
res[top] = i - top # i是栈顶右侧遇到的第一个比它高的索引
stack.append(i)
return res
84. 柱状图中最大的矩形 - 力扣(LeetCode)
这是第二遍做,虽然这道题依然没啥思路,但是对单调栈的理解还是变得比之前第一次做的时候要深刻多了。
基本思路:
遍历每一个heights中的高度,并利用单调栈在每个对应高度上获取该索引上左侧和右侧的边界, 从而获取到该索引高度对应的宽度,继而得到面积,并取最大面积。
如何获得当前索引高度最左侧和最右侧的索引呢?
用从栈底到栈顶递增的单调栈,一旦遇到比栈顶元素低的高度,立刻触发对栈顶的循环pop,直到当前待入栈元素入栈后依然能保持原本的单调性!
每次触发,相当于将当前一整段连续递增且比待入栈元素(短板)高的高度子序列提取出来,
从右向左从高到低一个一个计算当前高度对应的宽度乃至面积。
(连续重复元素需要使用技巧总是只入栈最新的索引,防止重复入栈)
每次触发,待入栈元素的索引right相当于这段递增的非连续子序列(非连续是因为我们记录的索引不一定连续)右边的第一个低点,
所以right-1
就是子序列最右侧(包含在子序列中)的索引,必然是子序列中每个高度的右边界。
同时在当次触发中,不断pop栈顶元素(令索引为peak_idx),相当于取出当前栈顶元素高度peak,该高度一定小于等于索引区间[peak_idx, right-1]中所有高度,
而peak_idx左侧也就是新的栈顶元素索引left处高度必然小于peak(因为我们已经去掉了站内的连续重复,当然不去的话也问题不大,想象一下即可知道为什么了),也就是说left处是peak_idx左边第一个低点!
所以实际上当前peak对应的宽度索引区间为(left, right-1]。
最后一个要点是:
由于最后抵达末尾之后,可能stack内还留有一个单调递增的子序列,所以需要在heights的最后手动添加dummy值-1,使得遍历到-1的时候能直接清空栈;
即heights.append(-1)
另外,又由于当栈中只有一个数的时候,pop出栈顶peak_idx,则栈变空,无法正常获取left = stack[-1]了!因此还需要为栈stack初始化一个-1元素,对应着索引0的左边索引,使得宽度计算也能正常进行。
即stack = [-1]
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
# 基本思路:遍历每一个heights中的高度,并利用单调栈在每个对应高度上获取该索引上左侧和右侧的边界,从而获取到该索引高度对应的宽度,继而得到面积,并取最大面积
# 如何获得当前索引高度最左侧和最右侧的索引呢?
# 用从栈底到栈顶递增的单调栈,一旦遇到比栈顶元素低的高度,立刻触发对栈顶的循环pop,直到当前待入栈元素入栈后依然能保持原本的单调性!
# 每次触发,相当于将当前一整段连续递增且比待入栈元素(短板)高的高度子序列提取出来,从右向左从高到低一个一个计算当前高度对应的宽度乃至面积。(连续重复元素需要使用技巧总是只入栈最新的索引,防止重复入栈)
# 每次触发,待入栈元素的索引right相当于这段递增的非连续子序列(非连续是因为我们记录的索引不一定连续)右边的第一个低点,所以right-1就是子序列最右侧(包含在子序列中)的索引,必然是子序列中每个高度的右边界。
# 同时在当次触发中,不断pop栈顶元素(令索引为peak_idx),相当于取出当前栈顶元素高度peak,该高度一定小于等于索引区间[peak_idx, right-1]中所有高度,
# 而peak_idx左侧也就是新的栈顶元素索引left处高度必然小于peak(因为我们已经去掉了站内的连续重复,当然不去的话也问题不大,想象一下即可知道为什么了),也就是说left处是peak_idx左边第一个低点!
# 所以实际上当前peak对应的宽度索引区间为(left, right-1]。
# 最后一个要点是:
# 由于最后抵达末尾之后,可能stack内还留有一个单调递增的子序列,所以需要在heights的最后手动添加dummy值-1,使得遍历到-1的时候能直接清空栈;
# 另外,又由于当栈中只有一个数的时候,pop出栈顶peak_idx,则栈变空,无法正常获取left = stack[-1]了!因此还需要为栈stack初始化一个-1元素,对应着索引0的左边索引,使得宽度计算也能正常进行。
heights.append(-1) # 由于最后抵达末尾之后,可能stack内还留有一个单调递增的子序列,所以需要在heights的最后手动添加dummy值-1,使得遍历到-1的时候能直接清空栈
stack = [-1] # 由于当栈中只有一个数的时候,pop出栈顶peak_idx,则栈变空,无法正常获取left = stack[-1]了!因此还需要为栈stack初始化一个-1元素,对应着索引0的左边索引,使得宽度计算也能正常进行。
res = 0 # 记录最大矩形面积
for i, h in enumerate(heights):
while stack[-1] != -1 and heights[stack[-1]] >= h:
# 第一个条件:stack[-1] != -1是为了防止pop到栈底-1时继续pop后导致引用stack[-1]出错,相当于常规单调栈中的while stack,这个条件也可以写为len(stack) > 1,效果一样;
# 第二个条件:取等号可以去掉栈中连续的重复高度,只记录最新的重复元素索引
peak_idx = stack.pop() # 当前峰值索引
peak = heights[peak_idx] # 矩形高度(峰值)
left, right = stack[-1], i # peak高度左边第一个低点索引和右边第一个低点索引
width = right - left - 1 # 矩形宽度
res = max(res, peak * width)
stack.append(i)
return res