一、栈(Stack):后进先出的世界
1.1 核心概念
栈 是一种后进先出 (LIFO, Last In First Out)的线性数据结构。所有操作仅限于一端------栈顶(Top)。
| 操作 | 说明 | 时间复杂度 |
|---|---|---|
push |
元素压入栈顶 | O(1) |
pop |
弹出栈顶元素 | O(1) |
peek / top |
查看栈顶(不移除) | O(1) |
is_empty |
判断栈是否为空 | O(1) |
生活类比:
- 一叠盘子:最后放上去的,最先被拿走
- 浏览器后退:最后访问的页面,最先回到
- 函数调用栈:
main()→funcA()→funcB(),funcB()最先返回
1.2 Python 实现
基于列表的实现(生产环境推荐 collections.deque 作为底层):
python
class Stack:
"""基于列表的栈实现"""
def __init__(self):
self._items: list = []
self._size: int = 0
def push(self, item) -> None:
"""入栈:O(1)"""
self._items.append(item)
self._size += 1
def pop(self):
"""出栈:O(1),栈空时抛出异常"""
if self.is_empty():
raise IndexError("pop from empty stack")
self._size -= 1
return self._items.pop()
def peek(self):
"""查看栈顶:O(1)"""
if self.is_empty():
raise IndexError("peek from empty stack")
return self._items[-1]
def is_empty(self) -> bool:
return self._size == 0
def size(self) -> int:
return self._size
def __repr__(self):
return f"Stack({self._items})"
# 验证
stack = Stack()
for x in [1, 2, 3]:
stack.push(x)
print(stack) # Stack([1, 2, 3])
print(stack.pop()) # 3
print(stack.peek()) # 2
⚠️ 注意 :Python 列表的
append()和pop()均摊时间复杂度为 O(1),但涉及动态扩容。对于超大规模数据,可使用collections.deque避免扩容开销。
1.3 经典应用一:括号匹配
问题 :判断字符串中的括号 ()、[]、{} 是否正确嵌套。
核心思想:左括号入栈,右括号与栈顶匹配。
python
def is_valid_parentheses(s: str) -> bool:
"""
有效括号判断
时间复杂度:O(n)
空间复杂度:O(n)
"""
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in '([{':
stack.append(char)
elif char in ')]}':
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()
return not stack # 栈空则完全匹配
# 测试用例
test_cases = [
("()", True),
("()[]{}", True),
("(]", False), # 类型不匹配
("([)]", False), # 顺序错误:交叉嵌套
("{[]}", True), # 正确嵌套
("((", False), # 左括号多余
("))", False), # 右括号多余
]
for s, expected in test_cases:
result = is_valid_parentheses(s)
status = "✅" if result == expected else "❌"
print(f"{status} {s:10} → {result} (期望 {expected})")
执行轨迹 (输入 "{[]}"):
| 步骤 | 字符 | 操作 | 栈状态 |
|---|---|---|---|
| 1 | { |
入栈 | ['{'] |
| 2 | [ |
入栈 | ['{', '['] |
| 3 | ] |
匹配 [,出栈 |
['{'] |
| 4 | } |
匹配 {,出栈 |
[] |
| 5 | 结束 | 栈空 → 合法 | --- |
1.4 经典应用二:逆波兰表达式求值
逆波兰表达式(RPN / 后缀表达式):运算符置于操作数之后,天然无需括号。
| 中缀表达式 | 后缀表达式 | 计算结果 |
|---|---|---|
(2 + 1) * 3 |
2 1 + 3 * |
9 |
4 + 13 / 5 |
4 13 5 / + |
6 |
(1 + 2) * (3 + 4) |
1 2 + 3 4 + * |
21 |
算法:遇数字入栈,遇运算符弹出两数计算,结果入栈。
python
def eval_rpn(tokens: list[str]) -> int:
"""
逆波兰表达式求值
时间复杂度:O(n)
空间复杂度:O(n)
"""
stack = []
operators = {'+', '-', '*', '/'}
for token in tokens:
if token not in operators:
stack.append(int(token))
else:
# 先弹出的是右操作数,后弹出的是左操作数
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b)
elif token == '*':
stack.append(a * b)
else:
# Python 的 // 是向下取整,题目要求向零取整
stack.append(int(a / b))
return stack[-1]
# 测试
print(eval_rpn(["2", "1", "+", "3", "*"])) # 9
print(eval_rpn(["4", "13", "5", "/", "+"])) # 6
print(eval_rpn(["10","6","9","3","+","-11","*","/","*","17","+","5","+"])) # 22
关键细节 :除法使用 int(a / b) 而非 a // b,因为 Python 的 // 对负数是向下取整(如 -3 // 2 == -2),而题目要求向零取整(-3 / 2 == -1)。
1.5 进阶应用:单调栈
单调栈是栈的重要扩展,用于高效寻找下一个更大/更小元素。
每日温度(Next Greater Element)
问题:给定温度数组,返回每天需等待几天才能遇到更高温度。
python
def daily_temperatures(temperatures: list[int]) -> list[int]:
"""
单调递减栈:维护一个温度递减的索引栈
时间复杂度:O(n) ------ 每个元素最多入栈、出栈一次
空间复杂度:O(n)
"""
n = len(temperatures)
result = [0] * n
stack = [] # 存索引,对应温度递减
for i in range(n):
# 当前温度打破递减趋势,说明找到了"下一个更大元素"
while stack and temperatures[i] > temperatures[stack[-1]]:
prev_idx = stack.pop()
result[prev_idx] = i - prev_idx # 天数差
stack.append(i)
return result
# 测试
temps = [73, 74, 75, 71, 69, 72, 76, 73]
print(daily_temperatures(temps))
# [1, 1, 4, 2, 1, 1, 0, 0]
栈的变化过程:
索引: 0 1 2 3 4 5 6 7
温度: 73 74 75 71 69 72 76 73
i=0: 栈=[0] (73)
i=1: 74>73, 弹出0, result[0]=1, 栈=[1] (74)
i=2: 75>74, 弹出1, result[1]=1, 栈=[2] (75)
i=3: 71<75, 栈=[2,3] (75,71)
i=4: 69<71, 栈=[2,3,4] (75,71,69)
i=5: 72>69, 弹出4, result[4]=1
72>71, 弹出3, result[3]=2, 栈=[2,5] (75,72)
i=6: 76>72, 弹出5, result[5]=1
76>75, 弹出2, result[2]=4, 栈=[6] (76)
i=7: 73<76, 栈=[6,7] (76,73)
剩余元素无更大值,保持0
二、队列(Queue):先进先出的秩序
2.1 核心概念
队列 是一种先进先出 (FIFO, First In First Out)的线性数据结构。一端(队尾 Rear)插入,另一端(队头 Front)删除。
| 操作 | 说明 | 时间复杂度 |
|---|---|---|
enqueue / put |
队尾插入 | O(1) |
dequeue / get |
队头移除 | O(1) |
peek / front |
查看队头 | O(1) |
is_empty |
判空 | O(1) |
生活类比:排队买票、打印机任务队列、消息队列(Kafka/RabbitMQ)、CPU进程调度。
2.2 Python 实现
绝对不要用 list.pop(0) 实现队列! 其时间复杂度为 O(n),因为需要移动所有后续元素。
python
from collections import deque
class Queue:
"""基于双端队列的高效实现"""
def __init__(self):
self._items: deque = deque()
self._size: int = 0
def enqueue(self, item) -> None:
"""入队:O(1)"""
self._items.append(item)
self._size += 1
def dequeue(self):
"""出队:O(1)"""
if self.is_empty():
raise IndexError("dequeue from empty queue")
self._size -= 1
return self._items.popleft()
def peek(self):
"""查看队头:O(1)"""
if self.is_empty():
raise IndexError("peek from empty queue")
return self._items[0]
def is_empty(self) -> bool:
return self._size == 0
def size(self) -> int:
return self._size
def __repr__(self):
return f"Queue({list(self._items)})"
# 验证
q = Queue()
for x in [1, 2, 3]:
q.enqueue(x)
print(q.dequeue()) # 1
print(q.dequeue()) # 2
print(q.peek()) # 3
2.3 循环队列(Circular Queue)
用固定大小数组实现,头尾指针循环移动,解决"假溢出"问题(数组末尾已满但前端有空位)。
python
class CircularQueue:
"""
循环队列:数组实现,空间固定
适用场景:缓冲区、生产者-消费者模型
"""
def __init__(self, capacity: int):
self._capacity = capacity
self._items = [None] * capacity
self._head = 0 # 队头索引
self._tail = 0 # 队尾下一个插入位置
self._size = 0 # 当前元素个数
def enqueue(self, item) -> None:
if self.is_full():
raise IndexError("enqueue into full queue")
self._items[self._tail] = item
self._tail = (self._tail + 1) % self._capacity
self._size += 1
def dequeue(self):
if self.is_empty():
raise IndexError("dequeue from empty queue")
item = self._items[self._head]
self._items[self._head] = None # 帮助垃圾回收
self._head = (self._head + 1) % self._capacity
self._size -= 1
return item
def peek(self):
if self.is_empty():
raise IndexError("peek from empty queue")
return self._items[self._head]
def is_empty(self) -> bool:
return self._size == 0
def is_full(self) -> bool:
return self._size == self._capacity
def size(self) -> int:
return self._size
# 测试:展示循环特性
cq = CircularQueue(3)
cq.enqueue(1)
cq.enqueue(2)
cq.enqueue(3)
print(f"满了吗?{cq.is_full()}") # True
print(f"出队: {cq.dequeue()}") # 1,head移到索引1
cq.enqueue(4) # tail循环到索引0
print(f"内部数组: {cq._items}") # [4, 2, 3] ------ 物理上不连续,逻辑上连续
2.4 经典应用一:二叉树层序遍历(BFS)
队列是广度优先搜索(BFS)的核心数据结构。
python
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def level_order(root: TreeNode) -> list[list[int]]:
"""
二叉树层序遍历
时间复杂度:O(n) ------ 每个节点访问一次
空间复杂度:O(w) ------ w为树的最大宽度
"""
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(current_level)
return result
# 构建测试树
# 3
# / \
# 9 20
# / \
# 15 7
root = TreeNode(3)
root.left = TreeNode(9)
root.right = TreeNode(20, TreeNode(15), TreeNode(7))
print(level_order(root))
# [[3], [9, 20], [15, 7]]
2.5 进阶应用:单调队列(滑动窗口最大值)
问题 :给定数组和窗口大小 k,返回每个滑动窗口中的最大值。
暴力解法 O(n*k) 会超时,使用单调递减队列 优化至 O(n)。
python
from collections import deque
def max_sliding_window(nums: list[int], k: int) -> list[int]:
"""
滑动窗口最大值 ------ 单调队列
时间复杂度:O(n)
空间复杂度:O(k)
"""
result = []
queue = deque() # 存索引,保持对应值单调递减
for i, num in enumerate(nums):
# 1. 移除窗口外的元素(队头过期)
while queue and queue[0] <= i - k:
queue.popleft()
# 2. 保持递减:移除所有小于当前值的元素(它们永无出头之日)
while queue and nums[queue[-1]] < num:
queue.pop()
queue.append(i)
# 3. 窗口形成后开始记录结果
if i >= k - 1:
result.append(nums[queue[0]])
return result
# 测试
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
print(max_sliding_window(nums, k))
# [3, 3, 5, 5, 6, 7]
核心思想:队列中维护候选最大值,确保队头始终是当前窗口最大值。新元素来时,所有比它小的旧元素都不可能成为最大值,直接剔除。
三、双端队列(Deque):栈与队列的融合
3.1 核心概念
双端队列(Double-ended Queue):两端均可进行插入和删除,兼具栈和队列的所有特性。
python
from collections import deque
dq = deque()
# 队尾操作(同栈/队列)
dq.append(1) # 队尾添加:O(1)
dq.append(2)
print(dq.pop()) # 队尾移除:O(1) → 2
# 队头操作(同队列)
dq.appendleft(0) # 队头添加:O(1)
print(dq.popleft()) # 队头移除:O(1) → 0
print(dq) # deque([1])
3.2 应用:回文检查
python
def is_palindrome(s: str) -> bool:
"""
双端队列检查回文
时间复杂度:O(n)
空间复杂度:O(n)
"""
# 过滤非字母数字字符,统一小写
dq = deque(c for c in s.lower() if c.isalnum())
while len(dq) > 1:
if dq.popleft() != dq.pop():
return False
return True
# 测试
print(is_palindrome("A man, a plan, a canal: Panama")) # True
print(is_palindrome("race a car")) # False
四、高级数据结构实战
4.1 最小栈(常数时间获取最小值)
要求 :支持 push、pop、top,并在 O(1) 时间内获取最小元素。
思路:辅助栈同步存储当前最小值。
python
class MinStack:
"""
最小栈
所有操作时间复杂度:O(1)
空间复杂度:O(n)
"""
def __init__(self):
self._stack = [] # 主栈:存储数据
self._min_stack = [] # 辅助栈:存储当前最小值
def push(self, val: int) -> None:
self._stack.append(val)
# 新元素小于等于当前最小值时,辅助栈同步入栈
# 注意用 <= 处理重复最小值
if not self._min_stack or val <= self._min_stack[-1]:
self._min_stack.append(val)
def pop(self) -> None:
val = self._stack.pop()
# 弹出的是当前最小值,辅助栈同步弹出
if val == self._min_stack[-1]:
self._min_stack.pop()
def top(self) -> int:
return self._stack[-1]
def get_min(self) -> int:
"""O(1) 获取最小值"""
return self._min_stack[-1]
# 验证
min_stack = MinStack()
min_stack.push(-2)
min_stack.push(0)
min_stack.push(-3)
print(min_stack.get_min()) # -3
min_stack.pop() # 弹出 -3
print(min_stack.top()) # 0
print(min_stack.get_min()) # -2
4.2 用栈实现队列
核心思想 :两个栈,in_stack 负责入队,out_stack 负责出队。当 out_stack 为空时,将 in_stack 全部倒入 out_stack(顺序反转,实现 FIFO)。
python
class StackQueue:
"""
用两个栈实现队列
均摊时间复杂度:O(1)
"""
def __init__(self):
self._in_stack = [] # 入队栈
self._out_stack = [] # 出队栈
def enqueue(self, x: int) -> None:
self._in_stack.append(x)
def dequeue(self) -> int:
self._shift_stacks()
return self._out_stack.pop()
def peek(self) -> int:
self._shift_stacks()
return self._out_stack[-1]
def is_empty(self) -> bool:
return not self._in_stack and not self._out_stack
def _shift_stacks(self) -> None:
"""将 in_stack 倒入 out_stack(仅在 out_stack 为空时)"""
if not self._out_stack:
while self._in_stack:
self._out_stack.append(self._in_stack.pop())
# 验证
sq = StackQueue()
sq.enqueue(1)
sq.enqueue(2)
print(sq.peek()) # 1
print(sq.dequeue()) # 1
print(sq.is_empty()) # False
print(sq.dequeue()) # 2
print(sq.is_empty()) # True
复杂度分析 :每个元素最多经历两次入栈(in_stack 和 out_stack)和两次出栈,因此均摊时间复杂度为 O(1)。
五、知识体系总结
5.1 栈 vs 队列:如何选择?
| 维度 | 栈(Stack) | 队列(Queue) |
|---|---|---|
| 核心原则 | LIFO(后进先出) | FIFO(先进先出) |
| 操作端 | 仅栈顶 | 队尾入,队头出 |
| 典型场景 | 撤销/后退、递归、表达式求值 | 排队、缓冲、BFS、任务调度 |
| 算法思想 | DFS、回溯、单调栈 | BFS、滑动窗口、单调队列 |
| Python实现 | list / deque |
collections.deque |
选择策略:
- 需要逆序处理 或嵌套结构?→ 用栈(括号匹配、递归转迭代)
- 需要公平调度 或层级遍历?→ 用队列(BFS、生产者-消费者)
- 需要两端操作?→ 用双端队列(滑动窗口、回文判断)
5.2 时间复杂度对比
| 操作 | 列表栈 | 列表队列 | 双端队列 |
|---|---|---|---|
| 入栈/入队 | O(1) | O(1) | O(1) |
| 出栈 | O(1) | --- | O(1) |
| 出队 | --- | O(n) ⚠️ | O(1) ✅ |
| 查看首尾 | O(1) | O(1) | O(1) |
⚠️ 关键警示 :Python 列表的
pop(0)是 O(n) 操作!队列请始终使用collections.deque。
5.3 单调结构速查
| 数据结构 | 单调性 | 典型题目 |
|---|---|---|
| 单调递增栈 | 栈底→栈顶 递增 | 下一个更小元素 |
| 单调递减栈 | 栈底→栈顶 递减 | 下一个更大元素(每日温度) |
| 单调递增队列 | 队头最小 | 滑动窗口最小值 |
| 单调递减队列 | 队头最大 | 滑动窗口最大值 |
核心心法 :栈解决的是"最近相关性"问题(匹配、嵌套),队列解决的是"时间顺序"问题(公平、层级)。理解数据的流动方向,比记住代码更重要。