ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python栈与队列实现:从list、deque到手动链表,性能与应用全解析

2026/8/4 5:57:58 拓冰建站 浏览量
Python栈与队列实现:从list、deque到手动链表,性能与应用全解析 1. 项目概述为什么栈和队列是程序员的“基本功”如果你刚开始学编程或者已经写过一些Python脚本可能会觉得“数据结构”这个词听起来有点吓人像是教科书里那些枯燥的理论。但今天我想聊的栈和队列恰恰相反它们是那种你每天都在用却可能没意识到的东西。想象一下你浏览网页时的“后退”按钮或者去食堂排队打饭的队伍——这些场景背后就是栈和队列在默默工作。我刚开始工作时也觉得这些概念离实际开发很远直到有一次调试一个复杂的函数调用链才真正体会到理解“函数调用栈”是多么重要。所以这篇内容不是来复述教科书的而是想从一个写过不少代码、也踩过不少坑的过来人角度跟你聊聊怎么在Python里亲手把栈和队列“造”出来以及更重要的是理解我们为什么要这么做。栈和队列是两种最基础、也最常用的线性数据结构。简单来说栈是一种“后进先出”的容器就像一摞盘子你总是从最上面取放而队列是一种“先进先出”的容器就像排队先来的人先得到服务。在Python的世界里虽然标准库list可以勉强模拟它们但直接使用list来实现栈和队列在特定场景下会有性能隐患或者语义不清晰的问题。自己动手实现一遍不仅能让你彻底理解它们的原理更能让你在面试、设计程序架构时清楚地知道该在什么时候、为什么选择它们。接下来我会从最基础的实现开始一步步深入到性能优化、应用场景和那些容易踩的坑目标是让你看完就能写出既正确又高效的代码。2. 核心思路从“能用”到“好用”的三种实现路径在动手写代码之前我们先得想清楚目标。实现一个数据结构最低要求是功能正确。但作为一个有追求的程序员我们还得考虑性能、易用性和安全性。对于栈和队列在Python中通常有三种实现思路每种都有其适用场景和权衡。2.1 路径一基于Python列表的快速原型这是最常见、最直观的起点。Python的list内置了append()和pop()方法完美契合栈的操作。对于队列虽然list的pop(0)操作可以模拟出队但它的时间复杂度是O(n)因为需要移动后面所有的元素。在数据量很小或者你只是写个demo验证想法时用list没问题代码简洁明了。但一旦数据量上来或者对性能有要求这就成了瓶颈。我早期写的很多脚本就是这么干的直到有一次处理一个上万条消息的日志队列程序卡得不行排查了半天才发现是pop(0)惹的祸。2.2 路径二使用collections.deque——标准库的“瑞士军刀”当你意识到list的性能问题时collections.deque双端队列就该登场了。它是Python标准库为这类场景准备的利器。deque的append、appendleft、pop、popleft操作都是近似O(1)的时间复杂度这意味着无论队列里有多少元素入队和出队操作都快如闪电。用它来实现栈和队列几乎就是“开箱即用”性能优异代码也很简洁。在绝大多数情况下这是我推荐的首选方案。它就像一把瑞士军刀可靠又高效。2.3 路径三手动实现链表结构——深入原理的终极修炼如果你想知道deque为什么这么快或者面试官想考察你对数据结构的理解深度那么手动实现一个基于链表的栈或队列就是必经之路。链表通过“节点”和“指针”在Python里是引用来连接数据添加和删除头尾节点不需要移动其他元素因此也能实现O(1)的操作。自己实现一遍你会对内存管理、引用关系有更深刻的认识。虽然在实际生产中你大概率会直接用deque但这个过程对于夯实基础、应对技术讨论至关重要。我当年就是为了弄懂一个内存泄漏问题才下定决心把链表的各种操作画图画到明白。选择哪条路径取决于你的需求。快速验证用list生产环境用deque学习原理就手动实现链表。下面我们就沿着从易到难的顺序把这三种实现都过一遍并重点聊聊其中的细节和坑。3. 核心细节解析三种实现方式的实操要点与避坑指南3.1 基于列表的实现简单背后的性能陷阱我们先从最简单的列表实现开始。对于栈实现起来非常直接。class StackWithList: def __init__(self): self._items [] # 用一个私有列表存储数据 def push(self, item): 入栈操作 self._items.append(item) # O(1) 时间复杂度 def pop(self): 出栈操作返回并移除栈顶元素 if self.is_empty(): raise IndexError(Pop from an empty stack) return self._items.pop() # 同样是 O(1) def peek(self): 查看栈顶元素但不移除 if self.is_empty(): raise IndexError(Peek from an empty stack) return self._items[-1] def is_empty(self): 判断栈是否为空 return len(self._items) 0 def size(self): 返回栈中元素个数 return len(self._items)看起来完美对吧对于栈Python的list确实很合适。但如果我们用同样的思路实现队列问题就来了class NaiveQueueWithList: def __init__(self): self._items [] def enqueue(self, item): 入队添加到队尾 self._items.append(item) # O(1) def dequeue(self): 出队从队头移除 if self.is_empty(): raise IndexError(Dequeue from an empty queue) return self._items.pop(0) # 问题所在O(n) # ... 其他方法如 is_empty, size, peek 类似关键陷阱pop(0)是元凶。每次从列表头部弹出一个元素Python都需要将后面所有的元素向前移动一位。当队列里有n个元素时这个操作的时间复杂度就是O(n)。如果你在一个循环里频繁调用dequeue()算法复杂度会从理想的O(n)恶化到O(n²)性能呈平方级下降。实操心得在Python中永远不要用list的pop(0)来实现一个需要处理大量数据的队列。这是初学者包括当年的我最容易犯的性能错误之一。在写任何涉及队列的代码前先问问自己数据量有多大。3.2 基于collections.deque的实现生产级的首选正因为看到了list的缺陷Python标准库提供了collections.deque。它的内部实现是一个双向链表无论从哪一端添加或删除元素都是常数时间复杂度。实现一个通用队列from collections import deque class EfficientQueue: def __init__(self): self._items deque() # 核心数据结构 def enqueue(self, item): 入队添加到队尾 self._items.append(item) # O(1) def dequeue(self): 出队从队头移除 if self.is_empty(): raise IndexError(Dequeue from an empty queue) return self._items.popleft() # O(1) def peek(self): 查看队头元素 if self.is_empty(): raise IndexError(Peek from an empty queue) # deque[0] 是查看队头元素的推荐方式不会改变deque return self._items[0] def is_empty(self): return len(self._items) 0 def size(self): return len(self._items)用deque同样可以轻松实现栈你只需要固定从同一端比如右端进行append和pop操作即可。class StackWithDeque: def __init__(self): self._items deque() def push(self, item): self._items.append(item) # 从右端入栈 def pop(self): if self.is_empty(): raise IndexError(Pop from an empty stack) return self._items.pop() # 从右端出栈 # ... 其他方法deque的高级特性与注意事项指定最大长度初始化deque(maxlenN)可以创建一个有长度限制的双端队列。当队列满时新加入的元素会自动从另一端挤出最老的元素。这个特性在实现“最近N条记录”这类功能时非常有用。线程安全deque的append()、pop()等操作是原子性的可以在多线程环境中安全使用前提是你不混合使用像len()这样的非原子操作。对于复杂的多线程场景还是需要额外的锁。内存效率虽然deque的每个元素都需要额外的内存来存储前后节点的引用导致其内存开销比list稍大但考虑到其卓越的头部操作性能这点开销在大多数情况下是值得的。避坑指南虽然deque很强大但要注意它的中间插入删除操作insert(i, item)或remove(value)仍然是O(n)的因为它需要遍历。deque的优势仅限于两端。3.3 手动实现链表节点理解一切的基础为了真正理解deque为何高效我们有必要看看链表是如何工作的。链表由一个个“节点”组成每个节点包含两部分数据域存储数据和指针域指向下一个节点。class Node: 定义链表节点 def __init__(self, data): self.data data # 节点存储的数据 self.next None # 指向下一个节点的引用初始为空这个简单的Node类是构建链表式栈和队列的基石。理解self.next None是关键它意味着每个节点在创建时都是独立的我们需要手动将它们“链”起来。4. 实操过程从零构建链表式栈与队列理解了节点我们就可以开始搭建了。手动实现能让你对边界条件如空链表、只有一个节点的链表的处理有肌肉记忆。4.1 实现链表栈维护一个“头指针”链表栈的关键是我们只关心栈顶。因此我们只需要一个指针通常叫top或head始终指向链表的第一个节点即栈顶。class LinkedListStack: def __init__(self): self._top None # 栈顶节点初始为空 self._size 0 # 额外维护一个长度变量避免每次遍历计数 def push(self, item): 入栈在链表头部插入新节点 new_node Node(item) # 1. 创建新节点 new_node.next self._top # 2. 新节点指向原栈顶 self._top new_node # 3. 更新栈顶指针为新节点 self._size 1 def pop(self): 出栈移除并返回链表头部节点 if self.is_empty(): raise IndexError(Pop from an empty stack) popped_node self._top # 1. 临时保存要弹出的节点 self._top self._top.next # 2. 栈顶指针指向下一个节点 self._size - 1 return popped_node.data # 3. 返回被弹出节点的数据 def peek(self): if self.is_empty(): raise IndexError(Peek from an empty stack) return self._top.data def is_empty(self): return self._top is None # 判断栈顶指针是否为空 def size(self): return self._size操作流程可视化以push(3)到已有[2-1]的栈为例创建新节点Node(3)其next为None。将新节点的next指向当前栈顶self._top即节点2。将self._top指针更新为新节点Node(3)。现在栈变成了[3-2-1]。这个过程中我们只改变了几个引用没有像列表那样移动任何数据所以是O(1)操作。4.2 实现链表队列需要“头尾两个指针”队列需要从一头进另一头出。因此我们需要两个指针_head或_front指向队头出队端_tail或_rear指向队尾入队端。class LinkedListQueue: def __init__(self): self._head None # 队头指针 self._tail None # 队尾指针 self._size 0 def enqueue(self, item): 入队在链表尾部添加新节点 new_node Node(item) if self.is_empty(): # 队列为空时新节点既是头也是尾 self._head new_node self._tail new_node else: # 队列不为空将当前尾节点的next指向新节点 self._tail.next new_node # 更新尾指针为新节点 self._tail new_node self._size 1 def dequeue(self): 出队移除并返回链表头部节点 if self.is_empty(): raise IndexError(Dequeue from an empty queue) dequeued_node self._head # 保存要出队的节点 self._head self._head.next # 头指针后移 self._size - 1 # 如果出队后队列为空需要将尾指针也置为None if self._head is None: self._tail None return dequeued_node.data def peek(self): if self.is_empty(): raise IndexError(Peek from an empty queue) return self._head.data def is_empty(self): return self._head is None def size(self): return self._size关键点解析初始化与空队列在__init__中头尾指针都设为None。判断队列为空的条件是self._head is None检查头指针即可。入队操作需要处理队列为空和非空两种情况。这是链表操作中常见的边界条件务必小心。非空时操作顺序是1) 链接旧尾节点2) 更新尾指针。出队操作出队后如果队列变空即self._head变成了None必须同步将self._tail也设为None。否则self._tail会变成一个“野指针”仍然指向已经被移出的节点内存这是内存管理中的一个细微但重要的点。实操心得在手动实现链表数据结构时画图拿张纸画出节点和指针一步步模拟enqueue和dequeue操作尤其是在处理空队列、单节点队列这些边界情况时。这比在脑子里空想有效十倍能帮你避免很多指针错乱的bug。5. 性能对比与选型建议如何做出明智的选择现在我们有了三种实现方式到底该用哪个我们从一个更全面的角度来对比一下。特性/实现方式基于 List (栈)基于 List (队列)基于 collections.deque手动链表实现入栈/入队时间复杂度O(1)O(1) (尾部追加)O(1)O(1)出栈时间复杂度O(1) (尾部弹出)O(n) (头部弹出)O(1)O(1)出队时间复杂度不适用O(n) (头部弹出)O(1)O(1)查看栈顶/队头O(1)O(1)O(1)O(1)空间开销较低连续内存较低连续内存中等每个元素带指针中等每个元素带指针代码复杂度极简简简中等功能丰富性基础基础丰富双端操作、最大长度等自定义适用场景学习原型、确信数据量极小的栈不推荐用于队列生产环境首选、通用栈/队列学习原理、面试、需要极端定制选型决策指南如果你在写一个快速脚本或原型并且只用栈用Python的list或者直接用list的方法append和pop连类都不用封装。这是最方便的。如果你需要实现一个队列或者一个可能用于生产环境的栈毫不犹豫地选择collections.deque。它是标准库为你优化好的工具性能可靠接口清晰。如果你在学习数据结构、准备面试或者需要实现一个非常特殊的、deque无法满足的行为那么手动实现链表版本是很好的练习。它能让你对指针、内存、边界条件有深刻的理解。一个常见的误区为了“优化”而盲目使用链表。在Python中由于list是基于动态数组的在尾部操作的性能非常好且内存局部性更佳数据在内存中连续存储CPU缓存命中率高。因此对于栈操作list和deque的性能在实际中差异不大list甚至可能因为缓存友好而略有优势。真正的性能分水岭在于队列的头部操作。所以记住这个简单的法则用list做栈用deque做队列。6. 高级应用与实战场景解析理解了基础实现我们来看看栈和队列在真实世界中的强大应用。这能帮你真正理解它们的价值。6.1 栈的典型应用场景函数调用栈这是栈最核心的应用。每次调用函数系统会将当前函数的返回地址、参数、局部变量等信息“压栈”函数返回时再“弹栈”恢复现场。递归函数深度过深导致的“栈溢出”错误就是因为这个调用栈被塞满了。括号匹配检查编译器检查()、{}、[]是否成对出现。遍历代码遇到左括号就入栈遇到右括号就检查栈顶是否是对应的左括号是则出栈否则报错。浏览器前进后退浏览器将访问过的URL压入一个栈后退栈。点击后退时当前URL被压入另一个栈前进栈并从后退栈弹出上一个URL。点击前进则相反。深度优先搜索在图和树的遍历中DFS通常使用栈来实现递归的本质也是栈。表达式求值与转换将中缀表达式如34*2转换为后缀表达式逆波兰表达式或者直接求值都需要栈来管理运算符的优先级。实战代码示例括号匹配检查器def is_valid_parentheses(s: str) - bool: stack [] # 这里用list作为栈就够了 mapping {): (, ]: [, }: {} # 右括号到左括号的映射 for char in s: if char in mapping.values(): # 如果是左括号 stack.append(char) elif char in mapping.keys(): # 如果是右括号 # 如果栈为空或栈顶不匹配则无效 if not stack or mapping[char] ! stack.pop(): return False # 其他字符可以忽略或者根据需求处理 # 最后栈必须为空所有左括号都被匹配了 return not stack6.2 队列的典型应用场景任务调度操作系统中的进程就绪队列、打印任务队列。先提交的任务先执行。消息队列在分布式系统中Kafka、RabbitMQ等消息中间件的核心模型就是队列。用于解耦生产者和消费者缓冲流量。广度优先搜索在图和树的遍历中BFS必须使用队列来保证“先访问的节点其邻居也先被访问”的顺序。缓存淘汰策略如FIFO先进先出缓存。也用于实现LRU最近最少使用缓存算法的辅助队列。实时系统数据流如网络数据包缓冲区、键盘敲击事件队列等确保事件按到达顺序被处理。实战代码示例使用队列进行二叉树的层序遍历from collections import deque class TreeNode: def __init__(self, val0): self.val val self.left None self.right None def level_order_traversal(root): 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 # 返回一个二维列表每一层是一个子列表7. 常见问题与排查技巧实录在实际编码和面试中关于栈和队列的问题远不止于实现。下面是我总结的一些高频问题和实战技巧。7.1 如何用栈实现队列如何用队列实现栈这是经典的面试题考察你对这两种数据结构本质的理解。问题一用两个栈实现队列思路维护两个栈stack_in负责入队stack_out负责出队。入队直接压入stack_in。出队如果stack_out为空则将stack_in中的所有元素依次弹出并压入stack_out。然后从stack_out弹出栈顶元素。关键只有当stack_out为空时才进行“倒腾”操作。每个元素最多被压入和弹出每个栈各一次因此均摊时间复杂度是O(1)。class QueueWithTwoStacks: def __init__(self): self.stack_in [] self.stack_out [] def enqueue(self, x): self.stack_in.append(x) def dequeue(self): if not self.stack_out: while self.stack_in: self.stack_out.append(self.stack_in.pop()) if not self.stack_out: # 倒腾后还是空说明队列空 raise IndexError(Dequeue from empty queue) return self.stack_out.pop()问题二用一个队列实现栈思路模拟栈的“后进先出”。每次入栈新元素后都将队列中已有的元素依次出队再入队除了刚加入的那个。这样队列的头部始终是最后加入的元素即栈顶。入栈将新元素入队然后将队列中之前的元素依次出队再入队循环size-1次。出栈直接出队即可。查看栈顶即查看队头。缺点入栈操作的时间复杂度是O(n)。from collections import deque class StackWithOneQueue: def __init__(self): self.q deque() def push(self, x): self.q.append(x) # 将新元素之前的元素全部移到它后面 for _ in range(len(self.q) - 1): self.q.append(self.q.popleft()) def pop(self): return self.q.popleft()7.2 循环队列解决普通队列的“假溢出”问题对于基于数组或固定大小列表实现的队列在经过多次入队和出队后即使数组前面有空位尾指针也可能指向数组末端导致无法继续入队这种现象叫“假溢出”。循环队列通过将数组视为一个环来解决这个问题。核心思想维护front和rear两个指针。当指针到达数组末尾时再前进就回到数组开头。判断队列满的条件(rear 1) % capacity front通常会牺牲一个存储单元来区分队空和队满。判断队列空的条件front rear。class CircularQueue: def __init__(self, k: int): self.capacity k 1 # 多分配一个空间用于判断队满 self.data [None] * self.capacity self.front 0 self.rear 0 def enqueue(self, value: int) - bool: if self.is_full(): return False self.data[self.rear] value self.rear (self.rear 1) % self.capacity return True def dequeue(self) - bool: if self.is_empty(): return False self.front (self.front 1) % self.capacity return True def get_front(self): if self.is_empty(): return -1 return self.data[self.front] def get_rear(self): if self.is_empty(): return -1 # rear指向的是下一个空位队尾元素在它前一个位置 return self.data[(self.rear - 1 self.capacity) % self.capacity] def is_empty(self): return self.front self.rear def is_full(self): return (self.rear 1) % self.capacity self.front排查技巧调试循环队列时最容易出错的就是队头和队尾指针的移动以及队空队满的判断。一个有效的方法是在每次enqueue和dequeue操作后都打印出整个数组、front和rear的值手动模拟几次就能理清逻辑。7.3 优先级队列不只是先进先出有时候队列中的元素需要按照优先级出队而不是简单的先进先出。这就是优先级队列通常用“堆”这种数据结构来实现。Python标准库提供了heapq模块来实现最小堆可以很方便地构建优先级队列。import heapq class PriorityQueue: def __init__(self): self._heap [] self._index 0 # 用于处理优先级相同时的排序 def push(self, item, priority): # heapq 实现的是最小堆所以用优先级和索引组成元组 # 优先级小的先出队 heapq.heappush(self._heap, (priority, self._index, item)) self._index 1 def pop(self): if not self._heap: raise IndexError(Pop from an empty priority queue) _, _, item heapq.heappop(self._heap) return item使用场景任务调度高优先级任务先执行、Dijkstra最短路径算法、哈夫曼编码等。7.4 线程安全与生产环境考量在我们之前的实现中都没有考虑多线程同时访问的情况。在生产环境中如果栈或队列会被多个线程共享就必须考虑线程安全。list和手动链表实现绝对不是线程安全的。并发修改会导致数据损坏或程序崩溃。collections.deque它的append()、pop()、popleft()等单方法操作是原子的因此对于简单的“单生产者-单消费者”模式如果每个线程只操作一端可能是安全的。但混合操作如一个线程append另一个线程读len或复杂的逻辑如“检查非空然后弹出”仍然需要锁。标准解决方案使用queue模块。queue.Queue: 线程安全的FIFO队列内部使用了锁和条件变量。queue.LifoQueue: 线程安全的LIFO栈。queue.PriorityQueue: 线程安全的优先级队列。import queue import threading def worker(q): while True: item q.get() # 线程安全地获取项目 if item is None: # 终止信号 break print(fProcessing {item}) q.task_done() # 主线程 q queue.Queue() threads [] for i in range(3): t threading.Thread(targetworker, args(q,)) t.start() threads.append(t) for item in range(10): q.put(item) q.join() # 等待所有任务完成 for _ in range(3): q.put(None) # 发送终止信号 for t in threads: t.join()结论在并发编程中不要自己造轮子优先使用queue模块提供的线程安全队列。从最基础的列表操作到标准库的高效deque再到深入原理的手动链表实现我们完整地走过了栈和队列在Python中的实现之路。我个人的体会是学习数据结构实现一遍只是第一步更重要的是理解每种结构背后的权衡list的连续内存带来了缓存友好性但头部操作是短板deque的链表结构牺牲了一点空间和中间操作的性能换来了两端操作的极致高效手动实现则是对指针和内存管理最好的训练。在实际项目中我的选择策略非常明确需要队列就用collections.deque或queue.Queue如果涉及多线程需要栈可以简单用list除非有非常特殊的定制化需求否则绝不轻易手动实现。最后多思考它们的应用场景比如用栈去做回溯和深度探索用队列去做缓冲和广度搜索这才是让这些知识“活”起来的关键。