栈(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
相当于删除链表头节点并返回其值。大小可以灵活增长,但每个节点有额外指针开销。
- 数组 (Array): 使用一个数组和一个指向栈顶的索引(通常初始化为 -1 表示空栈)。
- 时间复杂度: 所有核心操作 (
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): 将数组视为首尾相连的环。
front
和rear
在到达数组末尾时会绕回到开头。高效利用空间,是固定大小队列的常用实现。需要处理队列满/空的条件(通常通过一个计数器或区分rear+1 == front
表示满和rear == front
表示空)。
- 简单数组:
-
- 数组 (Array): 使用一个数组和两个索引:
front
指向队头元素,rear
指向下一个要插入的位置(队尾)。 - 链表 (Linked List): 使用单链表或双链表。维护两个指针:
head
指向链表的第一个节点(队头),tail
指向链表的最后一个节点(队尾)。enqueue
在tail
后添加新节点并更新tail
,dequeue
移除head
节点并更新head
。大小灵活增长,是动态队列的首选实现。
- 数组 (Array): 使用一个数组和两个索引:
- 时间复杂度:
-
- 链表实现:
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) (链表和循环数组) |
关键区别要点
- 操作顺序规则: 栈是 LIFO(最后进来最先出去),队列是 FIFO(最先进来最先出去)。这是最本质的区别。
- 操作位置: 栈的所有操作(增删查)都集中在栈顶 这一个点。队列的操作分散在队尾(添加) 和队头(移除) 两个点。
- 行为类比: 栈是垂直的"叠加",队列是水平的"排队"。
理解栈和队列的核心在于牢牢把握 LIFO 和 FIFO 这两个基本原则,以及它们对操作位置和顺序的限制。它们在算法和系统设计中无处不在,是构建更复杂结构和逻辑的基础模块。