https://leetcode-cn.com/tag/stack/problemset/
20. 有效的括号
微软一面考过这题
扫描,左括号入栈,右括号与栈顶元素比较,如果能匹配上就消掉
class Solution:
def isValid(self, s: str) -> bool:
stack=collections.deque()
mapper={
')':'(',
'}':'{',
']':'[',
}
for c in s:
if c in mapper:
if not stack:
return False
if mapper[c]!=stack.pop():
return False
else:
stack.append(c)
return not stack # 这里写错
1249. 移除无效的括号
无效括号分两种情况:
)
:如果栈顶出现)
,说明多余了(
:如果迭代完成后stack非空,说明(
多余了基于这两点认识,我们可以写以下代码:
class Solution:
def minRemoveToMakeValid(self, s: str) -> str:
stack = []
s = list(s)
for i, c in enumerate(s):
if c == "(":
stack.append(i)
elif c == ")":
if stack:
stack.pop()
else:
s[i] = ''
for i in stack:
s[i] = ''
return ''.join(s)
这类题目都有一个共同的特点:
此时的栈顶元素
(注意不是刚刚pop的栈顶元素)726. 原子的数量
扫描每个字符:
class Solution:
def countOfAtoms(self, formula: str):
N = len(formula)
stack = [Counter()]
i = 0
while i < N:
if formula[i] == "(":
stack.append(Counter())
i += 1
elif formula[i] == ")":
top = stack.pop()
i += 1
i_start = i
while i < N and formula[i].isdigit():
i += 1
multi = int(formula[i_start:i] or 1)
for name, cnt in top.items():
stack[-1][name] += cnt * multi
else:
i_start = i
i += 1
while i < N and formula[i].islower():
i += 1
name = formula[i_start:i]
i_start = i
while i < N and formula[i].isdigit():
i += 1
multi = int(formula[i_start:i] or 1)
stack[-1][name] += multi
return "".join(name + (str(stack[-1][name]) if stack[-1][name] > 1 else '')
for name in sorted(stack[-1]))
1190. 反转每对括号间的子串
class Solution:
def reverseParentheses(self, s: str) -> str:
stack = [""]
for chr in s:
if chr == "(":
stack.append("")
elif chr == ")":
top = stack.pop()
stack[-1] += top[::-1]
else:
stack[-1] += chr
return stack[-1]
856. 括号的分数
一道机器学习岗位面试题:求圆上任取三个点组成锐角三角形的概率
class Solution:
def scoreOfParentheses(self, S: str) -> int:
stack = [0]
for c in S:
if c == "(":
stack.append(0)
else:
val = stack.pop()
if val == 0: # 可以视为“叶子结点”
delta = 1
else: # 可以视为“非叶子结点”
delta = val * 2
stack[-1] += delta
return stack[0]
394. 字符串解码
class Solution:
def decodeString(self, s: str) -> str:
stack = [["", 0]]
for c in s:
if c.isdigit():
stack[-1][1] = stack[-1][1] * 10 + int(c)
elif c == "[":
stack.append(["", 0])
elif c == "]":
string, _ = stack.pop()
times = stack[-1][1]
if times == 0:
times = 1
stack[-1][0] += string * times
stack[-1][1] = 0
else:
stack[-1][0] += c
return stack[0][0]
剑指 Offer 31. 栈的压入、弹出序列
946. 验证栈序列
面试题31. 栈的压入、弹出序列(模拟,清晰图解)
好家伙,看了题解才知道我的思路是反过来的,居然还能AC
class Solution:
def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool:
stack = []
i = 0
for num in pushed:
stack.append(num)
while stack and stack[-1] == popped[i]:
stack.pop()
i += 1
return not stack
150. 逆波兰表达式求值
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
token2op = {
'*': mul,
'-': sub,
'+': add,
'/': lambda x, y: int(x / y)
}
stack = []
for token in tokens:
try:
num = int(token)
except:
y = stack.pop() # 注意顺序是反过来的
x = stack.pop()
num = token2op[token](x, y)
finally:
stack.append(num)
return stack[0]
224. 基本计算器
如何想到用「栈」?思路来自于递归
只有加减号 + 小括号的话,边界条件比较简单,我们可以采用1.2 基于栈的模拟题
的思路,栈中元素为一个tuple,下标0表示sign,下标1表示res结果
这种思路基本正确,只需要微调就可以用在有 乘除号 的情况中了
class Solution:
def calculate(self, s: str) -> int:
# sign, res, num
stack = [[1, 0, 0]]
def update_num_to_res(stack):
# res += num * sign
stack[-1][1] += stack[-1][2] * stack[-1][0]
stack[-1][2] = 0
for c in s:
if c == "(":
stack.append([1, 0, 0])
elif c == ")":
update_num_to_res(stack) # 括号闭合时更新
_, res, _ = stack.pop()
stack[-1][2] = res # 当前括号中算出的res更新到栈顶的num中
elif c in ("-", "+"):
update_num_to_res(stack) # 遇到符号时更新
stack[-1][0] = 1 if c == "+" else -1
elif c.isdigit():
num = stack[-1][2]
num = num * 10 + int(c)
stack[-1][2] = num
update_num_to_res(stack) # 根节点解析结束时更新
return stack[0][1]
772. 基本计算器 III
注:
1+2*-3
无法通过
拆解复杂问题:实现计算器
做了两个修改:
sign
→ \rightarrow → op
(operator)res
→ \rightarrow → num_stack
(栈里面套栈,用来解决优先级的问题)return res
→ \rightarrow → return sum(num_stack)
class Solution:
def calculate(self, s: str) -> int:
# op, num_stack, num
stack = [['+', [], 0]]
operators = ['+', '-', '*', '/']
def update_num_to_res(stack):
# num_stack 对象引用,不用显式更新
op, num_stack, num = stack[-1]
if op == '+':
num_stack.append(num)
elif op == '-':
num_stack.append(-num)
elif op in ('*', '/'):
x = num_stack.pop()
y = num
if op == '*':
num_stack.append(x * y)
else:
num_stack.append(int(x / y))
# num需要显示更新,置为0
stack[-1][2] = 0
for c in s:
if c == "(":
# op, num_stack, num
stack.append(['+', [], 0])
elif c == ")":
update_num_to_res(stack) # 括号闭合时更新
_, res, _ = stack.pop()
stack[-1][2] = sum(res) # 当前括号中算出的res更新到栈顶的num中
elif c in operators:
update_num_to_res(stack) # 遇到符号时更新
stack[-1][0] = c
elif c.isdigit():
num = stack[-1][2]
num = num * 10 + int(c)
stack[-1][2] = num
update_num_to_res(stack) # 根节点解析结束时更新
return sum(stack[0][1])
1047. 删除字符串中的所有相邻重复项
class Solution:
def removeDuplicates(self, s: str) -> str:
stack = []
for c in s:
if stack and c == stack[-1]:
stack.pop()
else:
stack.append(c)
return "".join(stack)
1209. 删除字符串中的所有相邻重复项 II
class Solution:
def removeDuplicates(self, s: str, k: int) -> str:
stack = [1]
n = len(s)
res = s[0]
for i in range(1, n):
res += s[i]
if len(res) >= 2 and res[-1] == res[-2]:
stack[-1] += 1
else:
stack.append(1)
if stack[-1] == k:
stack.pop()
res = res[:-k]
return res
32. 最长有效括号
class Solution:
def longestValidParentheses(self, s: str) -> int:
n = len(s)
dp = [0] * n
ans = 0
for i in range(n):
if i > 0 and s[i] == ')':
if s[i - 1] == "(" and i - 2 >= 0:
dp[i] = dp[i - 2] + 2
else:
pre_len = dp[i - 1]
match_ix = i - pre_len - 1
if match_ix >= 0 and s[match_ix] == '(':
dp[i] = dp[i - 1] + 2
if match_ix - 1 >= 0:
dp[i] += dp[match_ix - 1]
ans = max(ans, dp[i])
return ans
32. 最长有效括号,辅助栈
class Solution:
def longestValidParentheses(self, s: str) -> int:
stack = [-1]
res = 0
for i, chr in enumerate(s):
if len(stack) > 1 and chr == ")" and s[stack[-1]] == "(":
stack.pop()
res = max(res, i - stack[-1])
else:
stack.append(i)
return res
面试题 03.05. 栈排序
猿辅导、美团、字节面试题——双栈排序
要求stack2(辅助栈)从栈底到栈顶是单调递增的
def stackSort(stack1):
stack2 = []
while stack1:
# 取出 【栈1】 的 【栈顶元素】
top = stack1.pop()
# 不满足【单调递增】的条件
# 看起来【stack2】从栈底到栈顶是【单调递减】的
while stack2 and stack2[-1] > top:
t = stack2.pop()
stack1.append(t) # pop 掉这个元素,扔 stack1 中
# 将 【栈1】 的 【栈顶元素】 push到【栈2】
stack2.append(top)
return stack2
155. 最小栈
剑指 Offer 30. 包含min函数的栈
最小栈 (辅助栈法,清晰图解)
时间复杂度 O ( 1 ) O(1) O(1),空间复杂度 O ( N ) O(N) O(N)
维护一个单调递减的辅助栈B:
因为栈是访问受限的线性数据结构,即只能从栈顶访问栈。所以只能访问栈顶。
如果这题改为最小队列(常数时间求minOfQueue),应该仿照单调栈的做法,维护一个单调递增的双端队列,这样如果一个比当前元素都要小的元素(新的min-value)入队后,会从队尾将队列中所有元素pop掉,min-value成为新的队头。
class MinStack:
def __init__(self):
"""
initialize your data structure here.
"""
self.A = []
self.B = []
def push(self, x: int) -> None:
# 栈A正常push
self.A.append(x)
# 如果栈B元素空,或者x小于栈B的栈顶,push到栈B
# 目的是维护栈B为一个单调递减(从栈底到栈顶)的栈
if (not self.B) or x <= self.B[-1]:
self.B.append(x)
def pop(self) -> None:
# 栈A正常出栈
top = self.A.pop()
# 如果A的出栈元素恰为栈B的栈顶元素,B也出栈
if self.B and top == self.B[-1]:
self.B.pop()
# A B 的peak函数正常执行
def top(self) -> int:
return self.A[-1]
def getMin(self) -> int:
return self.B[-1]
716. 最大栈
因为popMax
的存在,这个操作需要不断出栈直到找到最大元素,然后再push回来,时间复杂度为 O ( N ) O(N) O(N)
class MaxStack(list):
def push(self, x):
m = max(x, self[-1][1] if self else None)
self.append((x, m))
def pop(self):
return list.pop(self)[0]
def top(self):
return self[-1][0]
def peekMax(self):
return self[-1][1]
def popMax(self):
m = self[-1][1]
b = []
while self[-1][0] != m:
b.append(self.pop())
self.pop()
map(self.push, reversed(b))
return m
剑指 Offer 59 - II. 队列的最大值
如果是最小队列,应该用单调栈的方法维护一个单调递增的双端队列。在4.1阐述过
如果是最大队列,应该用单调栈的方法维护一个单调递减的双端队列。本题就是这个思路
class MaxQueue:
def __init__(self):
self.queue = collections.deque()
# monotonous queue
self.monoq = collections.deque()
def max_value(self) -> int:
return self.monoq[0] if self.monoq else -1
def push_back(self, value: int) -> None:
# 单调递减队列 (前一个元素大于等于后一个元素)
# fixme: 老是把if 写成 while
# 相当于: not self.monoq[-1] >= value 即不满足单调递减的条件
while self.monoq and self.monoq[-1] < value:
self.monoq.pop()
self.monoq.append(value)
self.queue.append(value)
def pop_front(self) -> int:
if not self.queue:
return -1
left = self.queue.popleft()
if self.monoq[0] == left:
self.monoq.popleft()
return left
84. 柱状图中最大的矩形
对于每个元素,往左找到第一个小于这个元素的下标,往右找到第一个小于这个元素的下标
从左往右,维护一个单调递增栈,这样当前元素入栈
前,栈中所有大于当前元素的元素
都pop掉了,只剩下小于
当前元素的最近元素
所以我们可以做两个栈出来:
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
# left -> right
stack = []
left = []
for i in range(n):
# 不满足【严格单调递增】
while stack and (not heights[stack[-1]] < heights[i]):
stack.pop()
left.append(stack[-1] if stack else -1)
stack.append(i)
print(left)
# right -> left
stack = []
right = [n for i in range(n)]
for i in range(n - 1, -1, -1):
while stack and (not heights[stack[-1]] < heights[i]):
stack.pop()
# 用append就错啦
right[i] = (stack[-1] if stack else n)
stack.append(i)
print(right)
# calc
ans = 0
for i in range(n):
ans = max(ans, (right[i] - left[i] - 1) * heights[i])
return ans
我们可以将2个单调栈 + 2次遍历 化简为 1个单调栈 + 1次遍历 :
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
# left -> right
stack = []
left = [-1 for i in range(n)]
right = [n for i in range(n)]
for i in range(n):
# 不满足【严格单调递增】
while stack and (not heights[stack[-1]] < heights[i]):
right[stack.pop()] = i
left[i] = (stack[-1] if stack else -1)
stack.append(i)
ans = 0
for i in range(n):
ans = max(ans, (right[i] - left[i] - 1) * heights[i])
return ans
注:[01矩阵]中的 最大正方形 用DP做:
221. 最大正方形
85. 最大矩形
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
N = len(heights)
left = [0] * N
right = [N] * N
stack = []
for i in range(N):
while stack and heights[stack[-1]] > heights[i]:
right[stack.pop()] = i
left[i] = stack[-1] if stack else -1
stack.append(i)
return max(heights[i] * (right[i] - left[i] - 1) for i in range(N))
def maximalRectangle(self, matrix: List[List[str]]) -> int:
N = len(matrix)
if not N:
return 0
M = len(matrix[0])
if not M:
return 0
heights = [0] * M
ans = 0
for i in range(N):
for j in range(M):
if matrix[i][j] == "1":
heights[j] += 1
else:
heights[j] = 0
ans = max(ans, self.largestRectangleArea(heights))
return ans
496. 下一个更大元素 I
在5.1.1.1 柱状图中最大的矩形
中,我们讨论了:要找到左边第一个更小元素,需要维护一个单调递增栈。
而要找下一个最大元素,应该从右往左,维护一个单调递减栈。这样新元素入栈前,会把所以小于新元素的元素pop掉(保证新元素<栈顶元素,从而满足单调递减),此时的栈顶元素就是要找的下一个最大元素
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums = nums2
mp = {}
# ---------------------
# 单调递减栈
stack = []
L = len(nums)
ans = [0] * L
for i in range(L - 1, -1, -1):
num = nums[i]
# 不满足【单调递减】
while stack and (not stack[-1] > num):
stack.pop()
ans[i] = stack[-1] if stack else -1
stack.append(num)
# 缓存
mp[num] = ans[i]
# -----------------------
return [mp[x] for x in nums1]
739. 每日温度
这题和【下一个更大元素】基本一致
class Solution:
def dailyTemperatures(self, T: List[int]) -> List[int]:
nums = T
# -------------------------
# 单调递减栈
stack = []
L = len(nums)
ans = [0] * L
for i in range(L - 1, -1, -1):
num = nums[i]
# 不满足【单调递减】
while stack and (not nums[stack[-1]] > num):
stack.pop()
ans[i] = stack[-1] - i if stack else 0
stack.append(i)
return ans
难度升级为【环形】的题目还有:
打家劫舍2
环形的【跳台阶】,一个字节面试题
环形取石子,区间DP
503. 下一个更大元素 II
[1, 2, 1, 1, 2, 1]
[2, -1, 2 , 2 ,-1 ,-1]
从右往左,覆盖老元素
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
stack = []
L = len(nums)
ans = [0] * L
for i in range(L * 2 - 1, -1, -1):
num = nums[i % L]
while stack and (not stack[-1] > num):
stack.pop()
ans[i % L] = stack[-1] if stack else -1
stack.append(num)
return ans
这种类型的题就比5.1.1
,5.1.2
这样的题更简单,或者说更直白一些:
402. 移掉K位数字
移除这个数中的 k 位数字,使得剩下的数字最小。
首先要理解题意, 求 N − K N-K N−K个最小的数
思维转变, 把丢弃视为保留
删除第一个不单调递增
(开始下降, num[x] < num[x-1]
)
class Solution:
def removeKdigits(self, num: str, k: int) -> str:
# 单调递增栈
numStack = []
for digit in num:
# 相比普通单调栈,多个k这个判断条件
# 不满足于【单调递增】
while k and numStack and (not numStack[-1] <= digit):
numStack.pop()
k -= 1 # 最多只能pop k个,等同于保留N-K个
numStack.append(digit)
# 特殊情况: 单调递增的num
# [:-k] 等同于删除末尾的k个数字
finalStack = numStack[:-k] if k else numStack
# 特殊情况:前导0
# or "0" 这步相当妙,默写的时候没默出来
return "".join(finalStack).lstrip("0") or "0"
321. 拼接最大数
出现频率较小,有空再研究一下这题
class Solution:
def maxNumber(self, nums1: List[int], nums2: List[int], k: int) -> List[int]:
def pick_max(lst, k):
stack = []
drop = len(lst) - k # 没想到
for e in lst:
# 没想到
while drop and stack and stack[-1] < e:
stack.pop()
drop -= 1 # 没想到
stack.append(e)
return stack[:k] # 没想到截断 (2次)
def merge(la, lb):
res = []
# 为的是解决 [6, 7], [6, 0, 4] 这样的case
while la or lb:
# bigger 保证不为空列表
bigger = la if la > lb else lb
res.append(bigger.pop(0)) # 简写
# bigger.pop(0) # 简写
return res
ret = []
for sp in range(k + 1):
# 判断条件的 <= 写错为 <
if sp <= len(nums1) and k - sp <= len(nums2):
tmp = merge(
pick_max(nums1, sp),
pick_max(nums2, k - sp),
)
ret = max(ret, tmp)
return ret
316. 去除重复字母
1081. 不同字符的最小子序列
counter
表示剩余未遍历字符的出现次数
为什么用单调栈做?因为找出来的是子序列
如果去掉counter
的代码, 会造成使得每个字母只出现一次
的条件失效,即有的字母出现0次
counter
的作用是在删字母的时候,判断是否会导致有的字母不出现
class Solution:
def removeDuplicateLetters(self, s) -> int:
stack = []
counter = collections.Counter(s)
for c in s:
# 要求去重
if c not in stack:
# stack[-1] > c | 【严格单调递增】条件被破坏
# 需满足 counter[stack[-1]] > 0 的条件,即栈顶元素pop之后,还有替代的
while stack and (not stack[-1] < c) and counter[stack[-1]] > 0:
stack.pop()
stack.append(c)
# 遍历一个字符,做更新
counter[c] -= 1
return "".join(stack)
TODO: 理解还不深刻, 继续理解
在上一题中,限值条件是k
(所以出现在上一题的while
判断条件中),这一题的限值条件是使得每个字母只出现一次
,故判断条件是counter[stack[-1]]
。
counter
表示当前指针及之后所含元素的计数。如果从栈中弹出了元素,并且这个元素后续没有机会再添加进来了,这一定是非法的。
把 counter[mono_stack.back()]
写成了 counter[c]
,人没了
901. 股票价格跨度
class StockSpanner:
def __init__(self):
# 维护一个【单调递减】栈
self.stack = []
def next(self, price: int) -> int:
cnt = 1
# 不满足 单调递减
while self.stack and (not self.stack[-1][0] > price):
top = self.stack.pop()
cnt += top[1]
self.stack.append([price, cnt])
return cnt
剑指 Offer 59 - II. 队列的最大值
class MaxQueue:
def __init__(self):
self.queue = collections.deque()
# monotonous queue
self.monoq = collections.deque()
def max_value(self) -> int:
return self.monoq[0] if self.monoq else -1
def push_back(self, value: int) -> None:
# 单调递减队列 (前一个元素大于等于后一个元素)
# fixme: 老是把if 写成 while
while self.monoq and self.monoq[-1] < value:
self.monoq.pop()
self.monoq.append(value)
self.queue.append(value)
def pop_front(self) -> int:
if not self.queue:
return -1
left = self.queue.popleft()
if self.monoq[0] == left:
self.monoq.popleft()
return left
239. 滑动窗口最大值
剑指 Offer 59 - I. 滑动窗口的最大值
labuladong 单调队列解题详解
【Python】 简洁的单调队列解法(详解+注释)
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
queue = collections.deque()
N = len(nums)
res = []
for i in range(N):
# 满足单调递减
while queue and nums[queue[-1]] < nums[i]:
queue.pop() # 默认右端出栈
queue.append(i)
# 删掉左端不在滑动窗口内元素
if queue[0] <= i - k:
queue.popleft()
# 如果窗口已经形成,记录结果
if i >= k - 1:
# 结果记录的是最大值,所以需要把索引带入nums (默写出错)
res.append(nums[queue[0]])
return res
剑指 Offer 09. 用两个栈实现队列
入队,直接push到stack1
中即可
stack1:
-------------
| 1 2 3 4
-------------
出队,如果stack2
为空,将stack1
全部倒腾到stack2
中,然后取stack2
的栈顶
stack1:
-------------
|
-------------
stack2:
-------------
1 2 3 4 | (原来的栈顶变成栈底)
-------------
class CQueue:
def __init__(self):
self.stack1 = []
self.stack2 = []
def appendTail(self, value: int) -> None:
self.stack1.append(value)
def deleteHead(self) -> int:
if not self.stack2:
while self.stack1:
self.stack2.append(self.stack1.pop())
if self.stack2:
return self.stack2.pop()
return -1
225. 用队列实现栈
class MyStack {
Queue<Integer> queue;
/** Initialize your data structure here. */
public MyStack() {
queue=new LinkedList<>();
}
/** Push element x onto stack. */
public void push(int x) {
int n = queue.size();
queue.offer(x);
for (int i = 0; i < n; i++) {
queue.offer(queue.poll());
}
}
/** Removes the element on top of the stack and returns that element. */
public int pop() {
return queue.poll();
}
/** Get the top element. */
public int top() {
return queue.peek();
}
/** Returns whether the stack is empty. */
public boolean empty() {
return queue.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/