(d)队列接口与实现

官官方信息技术老师·15 页·深入(追求细节与边界)·0 次浏览·38 天前
数据结构接口设计实现对比边界行为

队列接口与实现

看完你能分清 Queue 各方法的语义差异,并亲手说清每种实现背后的取舍

按 空格/→ 演示下一步

问一句
1 / 15 页

全部页面点击任意一页,跳回舞台从这页播放

数据结构接口设计实现对比边界行为

队列接口与实现

看完你能分清 Queue 各方法的语义差异,并亲手说清每种实现背后的取舍

1第 1 页 · 队列接口与实现

排队里的队列

奶茶店高峰期排长队:店员永远先做队首那单,新来的只准排在队尾。没人插队、没人久等被跳过——这种「先来先服务」的秩序,正是队列的工作方式。

FIFO
First In, First Out:先进队的元素先出队
只动两端
队尾入队、队首出队,中间元素不可插手
公平的代价
无法插队也意味着不能随机访问中间元素
排队买奶茶对应 →队列

排队顺序对应入队顺序,取餐口对应队首出队

2第 2 页 · 排队里的队列

队列的接口

上一页把队列比作排队:先来的先走。但作为使用者,我们不关心队伍怎么摆,只关心能对它做什么——就像点餐只需要会看菜单,不必进后厨。这些「能做的操作」就是接口。

enqueue 入队
从队尾加入一个元素,像新来的人排到队尾
dequeue 出队
从队头取走一个元素,队头必须存在,空队取是错误
front 查看队头
只看不取,队伍不发生变化
empty 判空
出队前先问一句:还有人排着吗?
餐厅点餐对应 →队列接口

菜单上的菜名对应接口方法,后厨做法对应内部实现

3第 3 页 · 队列的接口

接口长什么样

python

用 Python 真实代码展示队列接口:五个操作各自的签名与职责。

代码高亮加载中…

enqueue 只动尾部、dequeue 只动头部,这就是 FIFO 在接口层面的体现;dequeue 和 peek 都先做空队列检查。

4第 4 页 · 接口长什么样

队列的典型实例

三种典型场景最终都汇入同一个队列结构,读图时注意数据都从队尾进、队头出。

图解渲染中…
q:先入先出的核心结构,三种场景共用同一规则
5第 5 页 · 队列的典型实例

接口自测

点击作答

向队列依次入队 1、2、3,再出队两次,每次出队得到的依次是?

6第 6 页 · 接口自测

实现即选择

前面见过队列接口的样子:入队、出队、看队头。但接口只说「做什么」,没说「怎么存」。就像同一份菜单,后厨用普通灶还是中央厨房,上菜速度完全不同——实现的选择决定性能。

数组实现
内存连续,按下标直达;但头部出队要搬移或用环形下标
链表实现
节点分散存储,头尾各留指针,出入队都只改指针
选择的权衡
看操作频率与内存布局,没有万能的最优实现
同一菜单不同后厨对应 →接口与实现分离

菜单是接口,后厨做法是实现,菜名一样出菜方式不同

7第 7 页 · 实现即选择

链表实现队列

链表用头指针+尾指针,让两端操作都只需改几个指针。

1
维护两个指针
head 指向队头,tail 指向队尾,两端都有直达地址
2
enqueue 尾插
tail.next 指向新节点,tail 后移,常数次操作
3
dequeue 头删
head 前移到下一个节点,旧节点被丢弃
4
空队列边界
出队后 head 为空则 tail 也要置空,否则残留野指针
8第 8 页 · 链表实现队列

数组实现的陷阱

用朴素数组出队,每移除队首都要求余元素前移,看空间和时间怎么被浪费。

1
初始状态
数组 [A,B,C,D],front 指下标 0,队尾在 3
2
A出队
A 离开后,B、C、D 必须整体前移一格补位
3
再出队
B 出队又要前移一次,每次出队都付出 O(n) 搬移代价
4
假溢出
若不移而只挪 front 指针,尾部很快顶到数组末端,前端却空着——假溢出
5
出路
把数组首尾逻辑相连成环形,用取模计算下标,空间即可复用
9第 9 页 · 数组实现的陷阱

循环数组与假溢出

从下标 0 出发看数组如何首尾相连:入队指针 tail 走到末尾后取模回绕,复用 head 让出的空位。

图解渲染中…
a0:tail 到达数组末尾,普通数组只能向后走a1:(tail+1) % n 回到 0,数组逻辑上首尾相接d1:满与空都指向同一位置,需牺牲一格或计数器区分
10第 10 页 · 循环数组与假溢出

循环队列的判满边界

python

两行判满条件对比:牺牲一格 vs 维护计数,看清指针重合时的歧义。

代码高亮加载中…

空与满时 head、tail 都可能重合,单靠指针无法区分。牺牲一格让 tail 永远停在 head 前一格,判满只看相邻关系;计数法则完整利用空间,代价是多维护一个变量。

11第 11 页 · 循环队列的判满边界

两种实现对比

两种实现都能实现队列,但时间、空间和硬件表现差异很大,选错会埋下性能隐患。

链表实现
  • 入队出队均 O(1),无扩容
  • 每节点多存一个指针,空间开销大
  • 节点散落内存,缓存不友好
循环数组
  • 均摊 O(1),扩容时单次 O(n)
  • 只存数据本身,空间紧凑
  • 内存连续,缓存命中率高
容量难预估、频繁增删选链表;容量可估、追求吞吐选循环数组。
12第 12 页 · 两种实现对比

要点收束

  • ✓接口是约定,实现是手段,二者解耦
  • ✓调用方只依赖接口,换实现不改业务代码
  • ✓链表实现无容量忧虑,数组实现更省内存
  • ✓数组实现的关键是循环数组解假溢出
  • ✓判满判空是循环队列最容易错的边界
延伸主题:双端队列与循环双端队列队列的阻塞与并发变体用队列实现 BFS 与任务调度
13第 13 页 · 要点收束

实现选择自测

点击作答

已知最大长度恒为 100 的固定任务池,需频繁入队出队且空间绝不浪费,最合适的实现是?

14第 14 页 · 实现选择自测

课后思考

先自己想一会儿,再对照参考答案——想的过程比答案本身更值钱。

1循环队列为什么必须牺牲一个存储位,或者额外维护一个计数字段?两者各牺牲了什么?

参考答案顺着判满边界想:front==rear 既可能是空也可能是满,必须打破这个歧义。牺牲一位是拿空间换简单;计数是拿每次入队出队的一次额外操作换空间。

2如果你要实现一个消息队列中间件,你会选链表还是循环数组?哪些真实需求会改变你的选择?

参考答案数组吞吐高、内存连续,但容量固定;消息量不可预估时链表按需分配更稳。真实需求如持久化、背压控制、动态扩容策略,都可能推翻课堂上的简单结论。

3双端队列允许两头进出,我们讲的『接口决定实现』还成立吗?哪种实现会更吃力?

参考答案接口变大,实现负担变重:数组两头都要 O(1) 就得改成循环双端,边界条件翻倍;链表需双向链表。接口每加一个操作,都在重新分配各实现的优势场景。

15第 15 页 · 课后思考
(d)队列接口与实现 · 知识图解