Python 算法基础篇之栈和队列

一、栈(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 最小栈(常数时间获取最小值)

要求 :支持 pushpoptop,并在 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_stackout_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 单调结构速查

数据结构 单调性 典型题目
单调递增栈 栈底→栈顶 递增 下一个更小元素
单调递减栈 栈底→栈顶 递减 下一个更大元素(每日温度)
单调递增队列 队头最小 滑动窗口最小值
单调递减队列 队头最大 滑动窗口最大值

核心心法 :栈解决的是"最近相关性"问题(匹配、嵌套),队列解决的是"时间顺序"问题(公平、层级)。理解数据的流动方向,比记住代码更重要。

相关推荐
YJlio8 小时前
Windows Internals 10.5.3:ETW 架构详解,从事件产生到性能分析的完整链路
windows·笔记·python·stm32·嵌入式硬件·学习·架构
敲上瘾8 小时前
LangChain 结构化输出与流式传输
python·语言模型·langchain·aigc
.柒宇.8 小时前
AI 掘金头条项目-用户模块、收藏模块以及Redis和调用大模型实现
redis·python·fastapi·千问·qwen大模型
艺术电影节8 小时前
惊喜映后 | 伍迪·艾伦经典修复澳门首映
算法·推荐算法·电视
yexuhgu8 小时前
Golang如何做贪心算法_Golang贪心算法教程【速学】
jvm·数据库·python
Brilliantwxx8 小时前
【C++】初步认识STL(3)
开发语言·c++·笔记·算法
玛丽莲茼蒿8 小时前
Leetcode hot100 螺旋矩阵【中等】
算法·leetcode·矩阵
EnCi Zheng8 小时前
M4-更新日志v0.1.3-Mermaid图表支持 [特殊字符]
python
Tisfy8 小时前
LeetCode 0396.旋转函数:求diff
算法·leetcode·题解·模拟·增量法