栈与队列数据结构详解

栈(Stack)和队列(Queue)这两种基础且非常重要的线性数据结构 。它们的核心区别在于数据元素的添加(插入)和移除(删除)遵循的规则不同

1. 栈 (Stack)

  • 核心理念: 后进先出 (Last-In-First-Out, LIFO) 。想象一摞盘子:你总是把新盘子放在最上面(添加),也总是从最上面拿走盘子(移除)。最后放上去的盘子,总是最先被拿走。
  • 操作限制: 所有的插入(push)和删除(pop)操作都只能在结构的一端进行,这一端被称为 栈顶 (Top) 。另一端称为 栈底 (Bottom) 。不能从中间或底部直接操作元素。
  • 核心操作:
    • push(item) / 入栈 将一个新元素添加到栈顶。栈的大小增加。
    • pop() / 出栈 移除并返回栈顶的元素。栈的大小减小。尝试从一个空栈 pop 通常会导致错误(栈下溢)。
    • peek() / top() / 查看栈顶 返回栈顶的元素,但不移除它。只是看一眼栈顶是什么。
    • isEmpty() / 判空 检查栈是否为空。
    • isFull() (仅在固定大小栈中需要): 检查栈是否已满(如果栈的大小是固定的)。
  • 类比:
    • 一摞盘子
    • 弹夹(子弹压入弹夹,射击时弹出最后压入的子弹)
    • 摞起来的书
    • 网页浏览器的"后退"按钮历史记录(最后访问的页面最先返回)
    • 文档编辑器的"撤销"(Undo) 功能(最后执行的操作最先撤销)
  • 实现方式:
    • 数组 (Array): 使用一个数组和一个指向栈顶的索引(通常初始化为 -1 表示空栈)。push 时索引递增并赋值,pop 时返回索引处值并递减索引。实现简单,访问快,但大小通常固定(或动态扩容有开销)。
    • 链表 (Linked List): 使用单链表,通常将链表的头节点 作为栈顶push 相当于在链表头部插入新节点,pop 相当于删除链表头节点并返回其值。大小可以灵活增长,但每个节点有额外指针开销。
  • 时间复杂度: 所有核心操作 (push, pop, peek, isEmpty) 在数组和链表实现中通常都是 O(1) 常数时间复杂度。
  • 关键特性:
    • 元素的添加和移除严格限制在栈顶一端。
    • 访问具有局部性:你只能直接访问栈顶元素。要访问栈底元素,必须先移除其上所有元素。
    • 顺序由添加顺序严格反向决定:最后添加的最先移除。
  • 典型应用:
    • 函数调用栈: 程序执行时记录函数调用链、局部变量、返回地址。调用函数时压栈,返回时出栈。
    • 表达式求值: 将中缀表达式转换为后缀/前缀表达式,以及计算后缀表达式(使用操作数栈和运算符栈)。
    • 括号匹配: 检查代码或表达式中括号 (), [], {} 是否成对且嵌套正确。
    • 撤销/重做机制 (Undo/Redo): 将用户操作按顺序压入栈,撤销时弹出并执行逆操作(可能需要两个栈)。
    • 深度优先搜索 (DFS): 在图或树的遍历中,使用栈来记录待访问的节点。
    • 回溯算法: 记录决策路径,当需要回退时从栈顶弹出状态。

2. 队列 (Queue)

  • 核心理念: 先进先出 (First-In-First-Out, FIFO) 。想象人们在超市收银台排队:新来的人排在队伍末尾(添加),排在队伍最前面的人最先被服务并离开队伍(移除)。最早排队的人,最早得到服务。
  • 操作限制: 元素的添加(enqueue)发生在结构的一端,称为 队尾 (Rear / Tail) 。元素的移除(dequeue)发生在结构的另一端,称为 队头 (Front / Head) 。不能从中间直接操作元素。
  • 核心操作:
    • enqueue(item) / 入队 / offer(item) 将一个新元素添加到队尾。队列的长度增加。
    • dequeue() / 出队 / poll() 移除并返回队头的元素。队列的长度减小。尝试从一个空队列 dequeue 通常会导致错误(队列下溢)。
    • peek() / front() / 查看队头 返回队头的元素,但不移除它
    • isEmpty() / 判空 检查队列是否为空。
    • isFull() (仅在固定大小队列中需要): 检查队列是否已满。
  • 类比:
    • 超市收银台排队
    • 打印机任务队列(先提交的文档先打印)
    • 客服电话等待队列(先打进来的电话先被接通)
    • 水管(水从一端流入,从另一端流出,先流入的水先流出)
  • 实现方式:
    • 简单数组: dequeue 后所有元素需要前移(时间复杂度 O(n),效率低)。不常用。
    • 循环数组 (Circular Buffer / Ring Buffer): 将数组视为首尾相连的环。frontrear 在到达数组末尾时会绕回到开头。高效利用空间,是固定大小队列的常用实现。需要处理队列满/空的条件(通常通过一个计数器或区分 rear+1 == front 表示满和 rear == front 表示空)。
    • 数组 (Array): 使用一个数组和两个索引:front 指向队头元素,rear 指向下一个要插入的位置(队尾)。
    • 链表 (Linked List): 使用单链表或双链表。维护两个指针:head 指向链表的第一个节点(队头),tail 指向链表的最后一个节点(队尾)。enqueuetail 后添加新节点并更新 taildequeue 移除 head 节点并更新 head。大小灵活增长,是动态队列的首选实现。
  • 时间复杂度:
    • 链表实现: enqueue, dequeue, peek, isEmpty 都是 O(1)
    • 循环数组实现: enqueue, dequeue, peek, isEmpty 也都是 O(1)
  • 关键特性:
    • 元素的添加和移除严格限制在两端:添加在队尾,移除在队头。
    • 元素的处理顺序严格遵循其到达的先后顺序:最早添加的最先移除。
  • 典型应用:
    • 任务调度: 操作系统进程调度(如先来先服务 FCFS)、线程池任务队列。
    • 消息队列: 分布式系统中,生产者将消息放入队列,消费者从队列中取出消息处理,实现解耦和异步通信(如 RabbitMQ, Kafka)。
    • 广度优先搜索 (BFS): 在图或树的遍历中,使用队列来记录待访问的节点(按层级遍历)。
    • 数据缓冲: 在数据传输速率不匹配的两个组件之间充当缓冲区(如 IO 缓冲区、网络数据包缓冲区)。生产者写入缓冲队列,消费者从缓冲队列读取。
    • 键盘缓冲区: 记录用户按下的按键顺序,供系统按序处理。
    • 打印任务管理: 多台计算机共享一台打印机时,打印任务按提交顺序排队等待。

栈 vs 队列 对比总结

特性 栈 (Stack) 队列 (Queue)
核心理念 LIFO (后进先出) FIFO (先进先出)
操作端 仅一端 (栈顶) push / pop 两端 enqueue(队尾) / dequeue(队头)
类比 一摞盘子、弹夹 排队、水管
添加操作 push() enqueue() / offer()
移除操作 pop() dequeue() / poll()
查看操作 peek() / top() peek() / front()
典型应用 函数调用栈、表达式求值、括号匹配、撤销操作、DFS 任务调度、消息队列、BFS、缓冲、打印队列
基本实现 数组、链表 循环数组、链表
核心操作时间复杂度 O(1) (push, pop, peek) O(1) (enqueue, dequeue, peek) (链表和循环数组)

关键区别要点

  1. 操作顺序规则: 栈是 LIFO(最后进来最先出去),队列是 FIFO(最先进来最先出去)。这是最本质的区别。
  2. 操作位置: 栈的所有操作(增删查)都集中在栈顶 这一个点。队列的操作分散在队尾(添加)队头(移除) 两个点。
  3. 行为类比: 栈是垂直的"叠加",队列是水平的"排队"。

理解栈和队列的核心在于牢牢把握 LIFO 和 FIFO 这两个基本原则,以及它们对操作位置和顺序的限制。它们在算法和系统设计中无处不在,是构建更复杂结构和逻辑的基础模块。

相关推荐
掘金-我是哪吒36 分钟前
分布式微服务系统架构第156集:JavaPlus技术文档平台日更-Java线程池使用指南
java·分布式·微服务·云原生·架构
亲爱的非洲野猪1 小时前
Kafka消息积压的多维度解决方案:超越简单扩容的完整策略
java·分布式·中间件·kafka
wfsm1 小时前
spring事件使用
java·后端·spring
微风粼粼1 小时前
程序员在线接单
java·jvm·后端·python·eclipse·tomcat·dubbo
缘来是庄1 小时前
设计模式之中介者模式
java·设计模式·中介者模式
rebel2 小时前
若依框架整合 CXF 实现 WebService 改造流程(后端)
java·后端
代码的余温3 小时前
5种高效解决Maven依赖冲突的方法
java·maven
慕y2743 小时前
Java学习第十六部分——JUnit框架
java·开发语言·学习
paishishaba3 小时前
Maven
java·maven
张人玉4 小时前
C# 常量与变量
java·算法·c#