栈队列 模版题单
栈和队列
简介
栈的特点是后入先出
根据这个特点可以临时保存一些数据,之后用到依次再弹出来,常用于 DFS 深度搜索
队列一般常用于 BFS 广度搜索,类似一层一层的搜索
Stack 栈
min-stack
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
- 思路:用两个栈实现或插入元组实现,保证当前最小值在栈顶即可
class MinStack:def __init__(self):self.stack = []def push(self, x: int) -> None:if len(self.stack) > 0:self.stack.append((x, min(x, self.stack[-1][1])))else:self.stack.append((x, x))def pop(self) -> int:return self.stack.pop()[0]def top(self) -> int:return self.stack[-1][0]def getMin(self) -> int:return self.stack[-1][1]
evaluate-reverse-polish-notation
波兰表达式计算 > 输入: [“2”, “1”, “+”, “3”, “*”] > 输出: 9
解释: ((2 + 1) * 3) = 9
- 思路:通过栈保存原来的元素,遇到表达式弹出运算,再推入结果,重复这个过程
这段代码实现了逆波兰表达式(RPN,也称为后缀表达式)的求值。逆波兰表达式的特点是运算符紧跟在操作数之后,例如3 4 +
表示3+4
。解题的核心思路是使用栈来处理表达式,遇到操作数时入栈,遇到运算符时弹出操作数进行计算,并将结果压回栈中。
解题步骤解析
-
定义辅助计算函数
comp
:- 该函数接收两个操作数
or1
、or2
和一个运算符op
,返回计算结果。 - 除法处理:使用整数除法并手动处理符号,确保结果向零截断(例如
-3 // 2
在 Python 中为-2
,而题目要求为-1
)。
- 该函数接收两个操作数
-
初始化栈:
- 使用列表
stack
存储操作数。
- 使用列表
-
遍历表达式中的每个 token:
- 若为操作数:直接转换为整数并入栈。
- 若为运算符:
- 从栈中弹出两个操作数(注意顺序:先弹出的是右操作数
or2
,后弹出的是左操作数or1
)。 - 调用
comp
函数计算结果。 - 将计算结果压回栈中。
- 从栈中弹出两个操作数(注意顺序:先弹出的是右操作数
-
返回最终结果:
- 遍历结束后,栈中仅剩一个元素,即为表达式的值。
算法关键点
- 栈的应用:栈的后进先出(LIFO)特性天然适合处理后缀表达式。遇到运算符时,最近的两个操作数一定在栈顶。
- 操作数顺序:弹出操作数时,先弹出的是右操作数,后弹出的是左操作数(例如
3 4 -
表示3-4
,而非4-3
)。 - 除法截断:手动实现向零截断的除法,确保结果符合题目要求。
示例说明
假设输入表达式为 ["2", "1", "+", "3", "*"]
(对应中缀表达式 (2+1)*3
):
-
遍历过程:
- 遇到
2
:入栈,栈为[2]
。 - 遇到
1
:入栈,栈为[2, 1]
。 - 遇到
+
:弹出1
和2
,计算2+1=3
,入栈,栈为[3]
。 - 遇到
3
:入栈,栈为[3, 3]
。 - 遇到
*
:弹出3
和3
,计算3*3=9
,入栈,栈为[9]
。
- 遇到
-
结果:栈中仅剩
9
,返回9
。
复杂度分析
- 时间复杂度:O(n)
遍历每个 token 一次,每个操作(入栈、出栈、计算)均为 O(1)。 - 空间复杂度:O(n)
栈的最大深度为 n/2(当表达式中操作数和运算符交替出现时)。
总结
该算法通过栈高效地处理逆波兰表达式,确保每个运算符都能正确获取其操作数。关键在于理解栈与后缀表达式的匹配关系,以及对除法截断的特殊处理。这种方法简洁且高效,是处理后缀表达式的标准解法。
class Solution:def evalRPN(self, tokens: List[str]) -> int:def comp(or1, op, or2):if op == '+':return or1 + or2if op == '-':return or1 - or2if op == '*':return or1 * or2if op == '/':abs_result = abs(or1) // abs(or2)return abs_result if or1 * or2 > 0 else -abs_resultstack = []for token in tokens:if token in ['+', '-', '*', '/']:or2 = stack.pop()or1 = stack.pop()stack.append(comp(or1, token, or2))else:stack.append(int(token))return stack[0]
decode-string
给定一个经过编码的字符串,返回它解码后的字符串。
s = “3[a]2[bc]”, 返回 “aaabcbc”.
s = “3[a2[c]]”, 返回 “accaccacc”.
s = “2[abc]3[cd]ef”, 返回 “abcabccdcdcdef”.
- 思路:通过两个栈进行操作,一个用于存数,另一个用来存字符串
class Solution:def decodeString(self, s: str) -> str:stack_str = ['']stack_num = []num = 0for c in s:if c >= '0' and c <= '9':num = num * 10 + int(c)elif c == '[':stack_num.append(num)stack_str.append('')num = 0elif c == ']':cur_str = stack_str.pop()stack_str[-1] += cur_str * stack_num.pop()else:stack_str[-1] += creturn stack_str[0]
binary-tree-inorder-traversal
给定一个二叉树,返回它的中序遍历。
- reference
class Solution:def inorderTraversal(self, root: TreeNode) -> List[int]:stack, inorder = [], []node = rootwhile len(stack) > 0 or node is not None:if node is not None: stack.append(node)node = node.leftelse:node = stack.pop()inorder.append(node.val)node = node.rightreturn inorder
clone-graph
给你无向连通图中一个节点的引用,请你返回该图的深拷贝(克隆)。
- BFS
这段代码实现了图的深拷贝(克隆)功能,主要思路如下:
-
处理空图情况:如果输入的起始节点
start
为空,直接返回None
。 -
初始化访问字典和队列:
visited
字典用于记录已克隆的节点,键为原图节点,值为克隆节点。- 队列
bfs
用于广度优先搜索(BFS),初始包含起始节点start
。
-
BFS遍历图:
- 出队当前节点:从队列中取出节点
curr
,获取其克隆节点curr_copy
。 - 遍历邻接节点:对于
curr
的每个邻接节点n
:- 若未访问:创建
n
的克隆节点,存入visited
,并将n
加入队列待处理。 - 建立克隆邻接关系:将
n
的克隆节点(无论是否新创建)添加到curr_copy
的邻接列表中。
- 若未访问:创建
- 出队当前节点:从队列中取出节点
-
返回结果:最终返回起始节点的克隆节点
visited[start]
。
关键点:
- 避免重复克隆:通过
visited
字典确保每个节点仅被克隆一次。 - 邻接关系处理:在遍历过程中动态建立克隆节点间的邻接关系,保证图结构的正确性。
复杂度:
- 时间复杂度:O(N + M),N为节点数,M为边数。
- 空间复杂度:O(N),主要用于存储克隆节点和BFS队列。
class Solution:def cloneGraph(self, start: 'Node') -> 'Node':if start is None:return Nonevisited = {start: Node(start.val, [])}bfs = collections.deque([start])while len(bfs) > 0:curr = bfs.popleft()curr_copy = visited[curr]for n in curr.neighbors:if n not in visited:visited[n] = Node(n.val, [])bfs.append(n)curr_copy.neighbors.append(visited[n])return visited[start]
- DFS iterative
class Solution:def cloneGraph(self, start: 'Node') -> 'Node':if start is None:return Noneif not start.neighbors:return Node(start.val)visited = {start: Node(start.val, [])}dfs = [start]while len(dfs) > 0:peek = dfs[-1]peek_copy = visited[peek]if len(peek_copy.neighbors) == 0:for n in peek.neighbors:if n not in visited:visited[n] = Node(n.val, [])dfs.append(n)peek_copy.neighbors.append(visited[n])else:dfs.pop()return visited[start]
number-of-islands
给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
High-level problem: number of connected component of graph
- 思路:通过深度搜索遍历可能性(注意标记已访问元素)
class Solution:def numIslands(self, grid: List[List[str]]) -> int:if not grid or not grid[0]:return 0m, n = len(grid), len(grid[0])def dfs_iter(i, j):dfs = []dfs.append((i, j))while len(dfs) > 0:i, j = dfs.pop()if grid[i][j] == '1':grid[i][j] = '0'if i - 1 >= 0:dfs.append((i - 1, j))if j - 1 >= 0:dfs.append((i, j - 1))if i + 1 < m:dfs.append((i + 1, j))if j + 1 < n:dfs.append((i, j + 1))returnnum_island = 0for i in range(m):for j in range(n):if grid[i][j] == '1':num_island += 1dfs_iter(i, j)return num_island
largest-rectangle-in-histogram
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
- 思路 1:蛮力法,比较每个以 i 开始 j 结束的最大矩形,A(i, j) = (j - i + 1) * min_height(i, j),时间复杂度 O(n^2) 无法 AC。
class Solution:def largestRectangleArea(self, heights: List[int]) -> int:max_area = 0n = len(heights)for i in range(n):min_height = heights[i]for j in range(i, n):min_height = min(min_height, heights[j])max_area = max(max_area, min_height * (j - i + 1))return max_area
- 思路 2: 设 A(i, j) 为区间 [i, j) 内最大矩形的面积,k 为 [i, j) 内最矮 bar 的坐标,则 A(i, j) = max((j - i) * heights[k], A(i, k), A(k+1, j)), 使用分治法进行求解。时间复杂度 O(nlogn),其中使用简单遍历求最小值无法 AC (最坏情况退化到 O(n^2)),使用线段树优化后勉强 AC。
class Solution:def largestRectangleArea(self, heights: List[int]) -> int:n = len(heights)seg_tree = [None] * nseg_tree.extend(list(zip(heights, range(n))))for i in range(n - 1, 0, -1):seg_tree[i] = min(seg_tree[2 * i], seg_tree[2 * i + 1], key=lambda x: x[0])def _min(i, j):min_ = (heights[i], i)i += nj += nwhile i < j:if i % 2 == 1:min_ = min(min_, seg_tree[i], key=lambda x: x[0])i += 1if j % 2 == 1:j -= 1min_ = min(min_, seg_tree[j], key=lambda x: x[0])i //= 2j //= 2return min_def LRA(i, j):if i == j:return 0min_k, k = _min(i, j)return max(min_k * (j - i), LRA(k + 1, j), LRA(i, k))return LRA(0, n)
- 思路 3:包含当前 bar 最大矩形的边界为左边第一个高度小于当前高度的 bar 和右边第一个高度小于当前高度的 bar。
class Solution:def largestRectangleArea(self, heights: List[int]) -> int:n = len(heights)stack = [-1]max_area = 0for i in range(n):while len(stack) > 1 and heights[stack[-1]] > heights[i]:h = stack.pop()max_area = max(max_area, heights[h] * (i - stack[-1] - 1))stack.append(i)while len(stack) > 1:h = stack.pop()max_area = max(max_area, heights[h] * (n - stack[-1] - 1))return max_area
Queue 队列
常用于 BFS 宽度优先搜索
implement-queue-using-stacks
使用栈实现队列
class MyQueue:def __init__(self):self.cache = []self.out = []def push(self, x: int) -> None:"""Push element x to the back of queue."""self.cache.append(x)def pop(self) -> int:"""Removes the element from in front of queue and returns that element."""if len(self.out) == 0:while len(self.cache) > 0:self.out.append(self.cache.pop())return self.out.pop() def peek(self) -> int:"""Get the front element."""if len(self.out) > 0:return self.out[-1]else:return self.cache[0]def empty(self) -> bool:"""Returns whether the queue is empty."""return len(self.cache) == 0 and len(self.out) == 0
binary-tree-level-order-traversal
二叉树的层序遍历
class Solution:def levelOrder(self, root: TreeNode) -> List[List[int]]:levels = []if root is None:return levelsbfs = collections.deque([root])while len(bfs) > 0:levels.append([])level_size = len(bfs)for _ in range(level_size):node = bfs.popleft()levels[-1].append(node.val)if node.left is not None:bfs.append(node.left)if node.right is not None:bfs.append(node.right)return levels
01-matrix
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。
两个相邻元素间的距离为 1
- 思路 1: 从 0 开始 BFS, 遇到距离最小值需要更新的则更新后重新入队更新后续结点
class Solution:def updateMatrix(self, matrix: List[List[int]]) -> List[List[int]]:if len(matrix) == 0 or len(matrix[0]) == 0:return matrixm, n = len(matrix), len(matrix[0])dist = [[float('inf')] * n for _ in range(m)]bfs = collections.deque([])for i in range(m):for j in range(n):if matrix[i][j] == 0:dist[i][j] = 0bfs.append((i, j))neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1)]while len(bfs) > 0:i, j = bfs.popleft()for dn_i, dn_j in neighbors:n_i, n_j = i + dn_i, j + dn_jif n_i >= 0 and n_i < m and n_j >= 0 and n_j < n:if dist[n_i][n_j] > dist[i][j] + 1:dist[n_i][n_j] = dist[i][j] + 1bfs.append((n_i, n_j))return dist
- 思路 2: 2-pass DP,dist(i, j) = max{dist(i - 1, j), dist(i + 1, j), dist(i, j - 1), dist(i, j + 1)} + 1
class Solution:def updateMatrix(self, matrix: List[List[int]]) -> List[List[int]]:if len(matrix) == 0 or len(matrix[0]) == 0:return matrixm, n = len(matrix), len(matrix[0])dist = [[float('inf')] * n for _ in range(m)]for i in range(m):for j in range(n):if matrix[i][j] == 1:if i - 1 >= 0:dist[i][j] = min(dist[i - 1][j] + 1, dist[i][j])if j - 1 >= 0:dist[i][j] = min(dist[i][j - 1] + 1, dist[i][j])else:dist[i][j] = 0for i in range(-1, -m - 1, -1):for j in range(-1, -n - 1, -1):if matrix[i][j] == 1:if i + 1 < 0:dist[i][j] = min(dist[i + 1][j] + 1, dist[i][j])if j + 1 < 0:dist[i][j] = min(dist[i][j + 1] + 1, dist[i][j])return dist
补充:单调栈
顾名思义,单调栈即是栈中元素有单调性的栈,典型应用为用线性的时间复杂度找左右两侧第一个大于/小于当前元素的位置。
largest-rectangle-in-histogram
这段代码使用单调栈解决了"柱状图中最大矩形面积"问题,其核心思路如下:
问题分析
给定一组柱状图的高度,要求找到其中能够形成的最大矩形面积。每个柱子的宽度为1,矩形需基于柱子的高度。
算法思路
- 单调递增栈:维护一个栈,存储柱子的索引,确保栈中索引对应的高度单调递增。当遇到较小高度时,弹出栈顶元素并计算面积。
- 虚拟尾元素:在高度数组末尾添加一个
0
,确保所有柱子都能被弹出栈(包括最后一个非零高度的柱子)。 - 虚拟头元素:栈初始化为
[-1]
,作为边界处理,避免栈为空的情况。 - 遍历与弹栈:
- 遍历每个柱子,若当前高度小于栈顶高度,弹出栈顶元素并计算其面积。
- 宽度计算:当前索引
i
与新的栈顶索引stack[-1]
之间的距离减1(即i - stack[-1] - 1
)。 - 更新最大面积。
- 最终结果:遍历结束后,栈中元素全部弹出,得到全局最大面积。
关键步骤解释
- 单调栈的作用:确保栈中元素的高度递增,遇到较小高度时触发弹栈,计算以弹出元素为高的矩形面积(因为左右边界已确定)。
- 宽度计算:对于弹出的元素
cur
,其右边界为当前索引i
,左边界为新的栈顶stack[-1]
,宽度为i - stack[-1] - 1
。 - 时间复杂度:每个元素最多入栈和出栈一次,时间复杂度为O(n)。
示例演示
假设输入为[2, 1, 5, 6, 2, 3]
,添加尾部0
后变为[2, 1, 5, 6, 2, 3, 0]
:
- 遍历到
i=1
(高度1):弹出2
,面积为2 * (1 - (-1) - 1) = 2
。 - 遍历到
i=4
(高度2):弹出6
和5
,面积分别为6 * (4 - 2 - 1) = 6
和5 * (4 - 1 - 1) = 10
。 - 遍历到
i=6
(高度0):弹出所有剩余元素,计算面积3 * (6 - 4 - 1) = 3
、2 * (6 - 3 - 1) = 4
、1 * (6 - (-1) - 1) = 6
。 - 最大面积为10,对应高度5和宽度2的矩形。
总结
通过单调栈维护递增高度的索引,遇到较小高度时触发计算,确保每个柱子的最大可能矩形被遍历到。该方法高效且优雅,是解决此类问题的经典思路。
class Solution:def largestRectangleArea(self, heights) -> int:heights.append(0)stack = [-1]result = 0for i in range(len(heights)):while stack and heights[i] < heights[stack[-1]]:cur = stack.pop()result = max(result, heights[cur] * (i - stack[-1] - 1))stack.append(i)return result
trapping-rain-water
class Solution:def trap(self, height: List[int]) -> int:stack = []result = 0for i in range(len(height)):while stack and height[i] > height[stack[-1]]:cur = stack.pop()if not stack:breakresult += (min(height[stack[-1]], height[i]) - height[cur]) * (i - stack[-1] - 1)stack.append(i)return result
补充:单调队列
单调栈的拓展,可以从数组头 pop 出旧元素,典型应用是以线性时间获得区间最大/最小值。
sliding-window-maximum
求滑动窗口中的最大元素
代码思路解析
这段代码实现了一个滑动窗口最大值算法,即在给定数组 nums
和窗口大小 k
的情况下,返回每个窗口的最大值组成的列表。以下是详细的思路分析:
1. 特殊情况处理
N = len(nums)
if N * k == 0:return []if k == 1:return nums[:]
N * k == 0
:如果数组为空(N=0
)或窗口大小为 0(k=0
),直接返回空列表。k == 1
:如果窗口大小为 1,每个窗口的最大值就是该元素本身,直接返回原数组的拷贝。
2. 使用双端队列维护最大值索引
maxQ = collections.deque()
result = []
maxQ
:一个双端队列(deque
),用于存储当前窗口内可能成为最大值的元素的索引(而不是值)。result
:存储最终结果的列表。
3. 遍历数组,维护双端队列
for i in range(N):# 移除窗口外的元素if maxQ and maxQ[0] == i - k:maxQ.popleft()# 移除队列中比当前元素小的元素while maxQ and nums[maxQ[-1]] < nums[i]:maxQ.pop()# 将当前元素索引加入队列maxQ.append(i)# 当窗口形成时,记录当前窗口的最大值if i >= k - 1:result.append(nums[maxQ[0]])
(1) 移除窗口外的元素
if maxQ and maxQ[0] == i - k:maxQ.popleft()
maxQ[0]
是当前窗口最大值的索引。- 如果
maxQ[0] == i - k
,说明该索引已经不在当前窗口范围内(窗口范围为[i-k+1, i]
),需要从队列头部移除。
(2) 移除队列中比当前元素小的元素
while maxQ and nums[maxQ[-1]] < nums[i]:maxQ.pop()
- 从队列尾部开始,移除所有比当前元素
nums[i]
小的元素的索引。 - 因为这些元素在后续窗口中不可能成为最大值(当前元素
nums[i]
比它们大且存活时间更长)。 - 这样可以保证队列是单调递减的(队头是当前窗口的最大值)。
(3) 将当前元素索引加入队列
maxQ.append(i)
- 将当前索引
i
加入队列尾部。
(4) 记录当前窗口的最大值
if i >= k - 1:result.append(nums[maxQ[0]])
- 当
i >= k - 1
时,窗口已经形成(从索引 0 开始,前k-1
个元素不足以形成窗口)。 - 此时队列头部
maxQ[0]
就是当前窗口的最大值的索引,将其值nums[maxQ[0]]
加入结果列表。
4. 返回结果
return result
- 最终返回所有窗口的最大值列表。
示例演示
以 nums = [1, 3, -1, -3, 5, 3, 6, 7]
,k = 3
为例:
- 初始化:
maxQ = []
,result = []
- 遍历过程:
i=0
:maxQ = [0]
(值1
),窗口未形成。i=1
:移除nums[0]=1 < nums[1]=3
,maxQ = [1]
(值3
),窗口未形成。i=2
:移除nums[1]=3 > nums[2]=-1
,maxQ = [1, 2]
(值3, -1
),窗口形成,result = [3]
。i=3
:maxQ[0] == 0
(i-k=0
),移除maxQ[0]
,maxQ = [2]
;nums[2]=-1 > nums[3]=-3
,maxQ = [2, 3]
(值-1, -3
),result = [3, 3]
。i=4
:移除nums[2]=-1 < nums[4]=5
和nums[3]=-3 < 5
,maxQ = [4]
(值5
),result = [3, 3, 5]
。i=5
:nums[4]=5 > nums[5]=3
,maxQ = [4, 5]
(值5, 3
),result = [3, 3, 5, 5]
。i=6
:移除nums[4]=5 < nums[6]=6
和nums[5]=3 < 6
,maxQ = [6]
(值6
),result = [3, 3, 5, 5, 6]
。i=7
:移除nums[6]=6 < nums[7]=7
,maxQ = [7]
(值7
),result = [3, 3, 5, 5, 6, 7]
。
- 最终结果:
[3, 3, 5, 5, 6, 7]
。
时间复杂度分析
- 时间复杂度:
O(N)
,每个元素最多被加入和移除队列一次。 - 空间复杂度:
O(k)
,双端队列最多存储k
个元素。
这是一种高效的滑动窗口最大值算法!
class Solution:def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:N = len(nums)if N * k == 0:return []if k == 1:return nums[:]# define a max queuemaxQ = collections.deque()result = []for i in range(N):if maxQ and maxQ[0] == i - k:maxQ.popleft()while maxQ and nums[maxQ[-1]] < nums[i]:maxQ.pop()maxQ.append(i)if i >= k - 1:result.append(nums[maxQ[0]])return result
shortest-subarray-with-sum-at-least-k
class Solution:def shortestSubarray(self, A: List[int], K: int) -> int:N = len(A)cdf = [0]for num in A:cdf.append(cdf[-1] + num)result = N + 1minQ = collections.deque()for i, csum in enumerate(cdf):while minQ and csum <= cdf[minQ[-1]]:minQ.pop()while minQ and csum - cdf[minQ[0]] >= K:result = min(result, i - minQ.popleft())minQ.append(i)return result if result < N + 1 else -1
总结
- 熟悉栈的使用场景
- 后入先出,保存临时值
- 利用栈 DFS 深度搜索
- 熟悉队列的使用场景
- 利用队列 BFS 广度搜索
练习
- min-stack
- evaluate-reverse-polish-notation
- decode-string
- binary-tree-inorder-traversal
- clone-graph
- number-of-islands
- largest-rectangle-in-histogram
- implement-queue-using-stacks
- 01-matrix