当前位置:

首页 > 编程开发 > 如何用Python实现栈和队列?

如何用Python实现栈和队列?

使用列表实现栈高效,因append和pop操作均为O(1);但用列表实现队列时,pop(0)为O(n),性能差。应使用collections.deque实现队列,因其popleft为O(1)。封装类可提供更清晰接口和错误处理,适用于复杂场景。频繁出队或大数据量时优选deque,简单栈操作可选list。

使用列表实现栈高效,因append和pop操作均为O(1);但用列表实现队列时,pop(0)为O(n),性能差。应使用collections.deque实现队列,因其popleft为O(1)。封装类可提供更清晰接口和错误处理,适用于复杂场景。频繁出队或大数据量时优选deque,简单栈操作可选list。

如何用Python实现栈和队列?

在Python中实现栈(Stack)和队列(Queue)有几种常见且有效的方法,最直接的方式是利用Python内置的列表(list)或collections模块中的deque(双端队列)。这两种数据结构本身就具备了实现栈和队列所需的基本操作,只是在性能表现上各有侧重。

解决方案

实现栈(LIFO,后进先出)和队列(FIFO,先进先出)的核心在于控制元素的添加和移除顺序。

1. 使用Python列表实现栈

Python的list天生就非常适合作为栈。append()方法可以将元素添加到列表的末尾,而pop()方法则默认从列表末尾移除元素。这完美符合栈的LIFO特性。

# 栈的实现示例
stack = []

# 入栈 (Push)
stack.append('A')
stack.append('B')
stack.append('C')
print(f"入栈后: {stack}") # 输出: ['A', 'B', 'C']

# 出栈 (Pop)
item_c = stack.pop()
print(f"出栈 {item_c} 后: {stack}") # 输出: ['A', 'B']

item_b = stack.pop()
print(f"出栈 {item_b} 后: {stack}") # 输出: ['A']

# 查看栈顶元素 (Peek)
if stack:
    print(f"栈顶元素: {stack[-1]}") # 输出: 'A'

# 检查栈是否为空
print(f"栈是否为空: {not stack}") # 输出: False

2. 使用collections.deque实现队列

虽然列表也可以实现队列,但它在从列表头部移除元素时效率较低(list.pop(0)是O(n)操作)。这时,collections.deque(双端队列)就显得尤为出色了。deque支持在两端高效地添加和移除元素,其性能是O(1)。

from collections import deque

# 队列的实现示例
queue = deque()

# 入队 (Enqueue)
queue.append('Task1')
queue.append('Task2')
queue.append('Task3')
print(f"入队后: {queue}") # 输出: deque(['Task1', 'Task2', 'Task3'])

# 出队 (Dequeue)
task1 = queue.popleft() # 从左端移除
print(f"出队 {task1} 后: {queue}") # 输出: deque(['Task2', 'Task3'])

task2 = queue.popleft()
print(f"出队 {task2} 后: {queue}") # 输出: deque(['Task3'])

# 查看队首元素 (Peek)
if queue:
    print(f"队首元素: {queue[0]}") # 输出: 'Task3'

# 检查队列是否为空
print(f"队列是否为空: {not queue}") # 输出: False

Python内置列表作为栈和队列的性能瓶颈是什么?

说实话,用Python的内置list来模拟栈,也就是只用append()和pop()操作,效率是相当高的,这两种操作的时间复杂度都是O(1)。这意味着无论栈里有多少元素,添加或移除一个元素所需的时间基本是恒定的。所以,对于栈来说,list几乎没有性能瓶颈,非常适合。

但当list被用作队列时,情况就有点不一样了。队列的特性是先进先出,这意味着我们需要从列表的“头部”移除元素。如果你用list.pop(0)来模拟出队操作,那问题就来了。pop(0)这个操作,每次都会把列表中所有剩余的元素向前移动一位,以填补被移除元素留下的空位。想象一下,如果你的列表里有几百万个元素,每次出队都要移动几百万个元素,这无疑是个巨大的开销。它的时间复杂度是O(n),n是列表中元素的数量。随着队列的增长,性能会急剧下降,这在处理大量数据或高并发场景下是完全不可接受的。我个人就遇到过一些老项目,为了方便直接用list.pop(0)做队列,结果在生产环境跑起来就发现CPU占用居高不下,一查才发现是这里出了问题。所以,对于队列操作,尤其是频繁出队的情况,我强烈建议避免使用list.pop(0)。

除了内置类型,如何用类封装栈和队列,实现更清晰的接口和错误处理?

虽然直接使用list或deque很方便,但在更复杂的应用场景中,我们往往希望拥有更清晰、更具表达力的接口,并且能够处理一些边界情况,比如从空栈或空队列中取出元素。这时候,将栈和队列封装成独立的类就显得很有必要了。这样做不仅能提供更语义化的方法名(比如push、pop、enqueue、dequeue),还能在内部隐藏实现细节,并加入自定义的错误处理逻辑。

下面是两个简单的类封装示例:

class MyStack:
    def __init__(self):
        # 内部使用列表存储栈元素
        self._items = []

    def push(self, item):
        """元素入栈"""
        self._items.append(item)

    def pop(self):
        """元素出栈,如果栈为空则抛出错误"""
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()

    def peek(self):
        """查看栈顶元素,不移除"""
        if not self._items:
            raise IndexError("peek from empty stack")
        return self._items[-1]

    def is_empty(self):
        """判断栈是否为空"""
        return not self._items

    def size(self):
        """返回栈中元素数量"""
        return len(self._items)

    def __str__(self):
        return f"Stack({self._items})"

    def __repr__(self):
        return self.__str__()

# 使用自定义栈
s = MyStack()
s.push(10)
s.push(20)
print(f"我的栈: {s}")
print(f"栈顶元素: {s.peek()}")
print(f"出栈: {s.pop()}")
print(f"栈是否为空: {s.is_empty()}")
print(f"栈大小: {s.size()}")
# s.pop()
# s.pop() # 再次pop会引发IndexError


from collections import deque

class MyQueue:
    def __init__(self):
        # 内部使用collections.deque存储队列元素,保证高效性
        self._items = deque()

    def enqueue(self, item):
        """元素入队"""
        self._items.append(item)

    def dequeue(self):
        """元素出队,如果队列为空则抛出错误"""
        if not self._items:
            raise IndexError("dequeue from empty queue")
        return self._items.popleft()

    def peek(self):
        """查看队首元素,不移除"""
        if not self._items:
            raise IndexError("peek from empty queue")
        return self._items[0]

    def is_empty(self):
        """判断队列是否为空"""
        return not self._items

    def size(self):
        """返回队列中元素数量"""
        return len(self._items)

    def __str__(self):
        return f"Queue({list(self._items)})" # 转换为列表方便打印

    def __repr__(self):
        return self.__str__()

# 使用自定义队列
q = MyQueue()
q.enqueue('JobA')
q.enqueue('JobB')
print(f"我的队列: {q}")
print(f"队首元素: {q.peek()}")
print(f"出队: {q.dequeue()}")
print(f"队列是否为空: {q.is_empty()}")
print(f"队列大小: {q.size()}")
# q.dequeue()
# q.dequeue() # 再次dequeue会引发IndexError

通过类封装,我们不仅拥有了更直观的API,还可以在方法内部加入各种逻辑,比如在pop()或dequeue()之前检查是否为空,从而避免运行时错误,或者记录操作日志,甚至可以扩展成线程安全的版本。这让代码更健壮,也更易于维护和理解。

什么时候应该选择collections.deque而不是普通列表来模拟栈和队列?

这是一个很实际的问题,选择哪种数据结构确实需要根据具体场景来定。在我看来,collections.deque和普通列表各有其最佳使用场景,没有绝对的优劣,关键在于“合适”。

选择collections.deque的场景:

  1. 作为队列使用时,特别是需要频繁从头部移除元素: 这是deque最主要的优势。正如前面提到的,list.pop(0)的O(n)性能在处理大量数据时会成为瓶颈,而deque.popleft()是O(1)。如果你在实现广度优先搜索(BFS)、任务调度器、日志处理队列等需要频繁“出队”的场景,那么deque几乎是唯一的选择。
  2. 作为双端队列使用时: 如果你的需求不仅是栈或队列,而是需要在两端都进行高效的添加和移除操作,比如实现一个历史记录功能,既能添加新记录,又能回溯旧记录,deque的append(), appendleft(), pop(), popleft()都能提供O(1)的性能。
  3. 对性能有严格要求,且数据量可能很大: 当你的程序需要处理大量数据,并且对响应时间有较高要求时,deque的恒定时间复杂度优势就非常明显了。它避免了列表在中间或头部操作时可能出现的元素复制和移动开销。
  4. 需要固定大小的循环缓冲区: deque有一个maxlen参数,可以创建一个固定大小的双端队列。当队列满时,再添加新元素会自动从另一端移除最老的元素。这对于实现滑动窗口、最近访问列表等功能非常方便,省去了手动管理大小的麻烦。

选择普通列表(list)的场景:

  1. 作为栈使用时: 如果你仅仅是需要一个栈,只在列表的末尾进行append()和pop()操作,那么普通列表完全够用,而且其实现也更简洁直观。在这种情况下,list和deque的性能差异可以忽略不计。
  2. 数据量较小,且对性能要求不那么极致: 对于小规模数据,即使偶尔有list.pop(0)这样的O(n)操作,其影响也可能微乎其微。代码的简洁性和可读性有时比极致性能更重要。
  3. 需要频繁随机访问元素: 列表支持通过索引进行O(1)的随机访问(my_list[index]),而deque虽然也支持索引访问,但在内部实现上,它的随机访问效率不如列表。如果你除了栈/队列操作外,还需要频繁地根据索引访问中间元素,那么列表可能更合适。

总的来说,当涉及到队列操作,尤其是需要从头部移除元素时,collections.deque是Pythonic且高效的选择。而如果只是简单的栈操作,或者数据量不大且需要随机访问,那么列表的简洁性也很有吸引力。理解它们底层的工作原理和时间复杂度,能帮助我们做出更明智的技术决策。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
Python如何实现带有撤销功能的栈_基于两个列表模拟操作历史
Python如何实现带有撤销功能的栈_基于两个列表模拟操作历史

用两个列表分别存储当前数据与操作日志,通过反向执行逆操作实现撤销,可避免内存浪费。撤销后执行新操作需清空未来历史,且逆操作中不可复用公开的push/pop方法,防止循环记录。空栈时pop或undo应静默失败。

C#怎么使用stackalloc分配_C#栈上内存分配优化方法教程【高级】
C#怎么使用stackalloc分配_C#栈上内存分配优化方法教程【高级】

stackalloc是C#底层性能工具,必须配合Span使用,适用于零分配、短生命周期、固定大小连续内存场景。分配大小受线程栈限制,建议不超过64KB,超限会导致进程终止的StackOverflowException。不能跨async/await边界或存入字段,安全使用可提升热点路径吞吐3~5倍并降低GC压力。

C++如何获取并打印函数的调用地址偏移 _ 符号化堆栈信息技巧【进阶】
C++如何获取并打印函数的调用地址偏移 _ 符号化堆栈信息技巧【进阶】

在C++调试中,获取函数调用地址偏移需用返回地址减去函数起始地址。Linux下利用backtrace()与dladdr()计算偏移;Windows中SymFromAddr()返回的起始地址需加模块基址;离线解析addr2line需先减去模块基址。偏移计算必须区分返回地址与函数入口,避免符号化失败。

C++实现字符串中括号匹配算法 _ 栈结构在表达式解析的应用【详解】
C++实现字符串中括号匹配算法 _ 栈结构在表达式解析的应用【详解】

直截了当地说:用 std::stack 是最稳妥的选择,但必须处理好三类边界情况——空栈弹出、类型错配、遍历结束后栈非空;但凡漏掉一个,都会导致误判。这是括号匹配算法的核心共识,很多初学 C++ 的人在这上面栽过跟头。 为什么不能只靠 if (c == ')') 判断右括号? 仅仅比较字符是否相等?

C++实现高性能并发无锁栈Lock-Free Stack _ CAS原子操作实战【源码】
C++实现高性能并发无锁栈Lock-Free Stack _ CAS原子操作实战【源码】

无锁栈实现需解决ABA问题,核心是通过版本号机制将指针与计数器打包为128位数据进行CAS操作。push和pop必须正确设置next指针及内存序,节点分配建议使用线程局部内存池,延迟释放需采用hazardpointer或EBR技术。验证是否真正无锁需检查底层原子操作是否为lock-free,并通过破坏性测试及工具检测数据竞争。

Next.js + PostgreSQL + Prisma 全栈项目架构设计
Next.js + PostgreSQL + Prisma 全栈项目架构设计

基于Next.js、PostgreSQL和Prisma的全栈项目架构,核心包括垂直切片目录结构、PrismaSchema严格建模、按执行上下文区分数据获取策略(服务器组件直接查库、客户端组件走API路由)、环境变量隔离部署,以及通过Zod运行时校验、事务包裹和分级异常处理实现类型安全与纵深防御。

Go 语言中 runtime.Stack 追踪 Goroutine 堆栈
Go 语言中 runtime.Stack 追踪 Goroutine 堆栈

runtime.Stack默认仅捕获当前goroutine堆栈,4KB缓冲区易导致截断。需设置大缓冲区并将第二个参数设为true才能获取所有goroutine堆栈。死锁诊断中可安全使用,但高并发频繁调用会导致停顿。无法直接获取指定goroutine堆栈,需借助内部调用或调试工具。

如何通过数组实现单调栈结构实战解决搜索最近更大变量元素的性能
如何通过数组实现单调栈结构实战解决搜索最近更大变量元素的性能

使用数组实现单调栈可高效解决搜索最近更大元素问题,时间复杂度与空间复杂度均为O(n)。手动预分配数组大小,用整数指针控制栈顶,避免扩容开销与对象封装,提升性能。核心思路是维护单调递减栈,一次遍历完成所有查询并记录结果。

Linux怎么查看进程消耗的堆栈内存 Linux下gdb分析进程详解
Linux怎么查看进程消耗的堆栈内存 Linux下gdb分析进程详解

在排查Linux进程内存问题时,我们常常关注堆内存,但栈内存的异常同样不容忽视。一个递归函数失控,或者一个大型局部数组,都可能悄无声息地耗尽栈空间,导致进程崩溃。那么,如何准确查看进程消耗的栈内存呢? 直接查看 /proc/[pid]/status 中的 VmStk 字段 最直接的方法,是查看内核为

怎么通过分析 JVM 逃逸分析(Escape Analysis)理解为何局部对象可以在栈上分配
怎么通过分析 JVM 逃逸分析(Escape Analysis)理解为何局部对象可以在栈上分配

逃逸分析是JIT编译时的动态推断,用于判断对象是否可能被外部访问。只有确认对象无逃逸时,JIT才会进行标量替换等优化,将对象字段拆解为局部变量。优化需满足代码为热点且对象完全不逃逸,可通过编译日志与GC行为验证。编写代码时应避免对象逃逸,以自然获得JIT优化。

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程
PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
Postman macOS版
Postman macOS版
Mac

Postman是一套面向API全生命周期的开发与测试平台,可在Mac上发送和调试多种协议请求,管理集合、环境及认证信息,并完成脚本测试、模拟服务、监控、文档发布和团队协作。它适合开发、测试及运维人员使用,提供个人免费方案。

Docker Desktop macOS版
Docker Desktop macOS版
Mac

Docker Desktop 是 Docker 官方为 Mac 提供的容器开发环境,集成 Docker Engine、命令行工具、Compose、Build、Kubernetes 与图形化管理界面。

Android Studio macOS版
Android Studio macOS版
Mac

Android Studio 是 Google 面向安卓应用开发提供的官方集成开发环境,基于 IntelliJ IDEA 构建。它整合智能代码编辑、Gradle 构建、界面预览、设备模拟、性能分析、调试测试及 APK 分析等工具。

WINDOWS 更多
PixPix 网页版
PixPix 网页版
Windows

PixPix 是面向品牌、卖家和内容团队的在线 AI 图片视频创作平台,提供图视频 Agent、无限画布、图片生成、视频生成、自然语言编辑及商品套图、产品精修和带货视频等电商工具。

Postman Windows版
Postman Windows版
Windows

Postman 是面向 API 全生命周期的开发与协作平台。Windows 桌面版可用于构建和发送 HTTP、GraphQL、gRPC、WebSocket、MQTT 等请求,并提供环境变量、集合、脚本测试、模拟服务、监控、文档及团队工作区等

Docker Desktop Windows版
Docker Desktop Windows版
Windows

Docker Desktop 为 Windows 开发者提供集成式容器环境,内置 Docker Engine、命令行工具、Compose、Build 与 Kubernetes 等组件,可在本机完成镜像构建、容器运行、日志查看和资源管理。