去做题

第九单元:队列操作 · 先进先出与任务调度

Unit 9: Queue Operations — FIFO & Task Scheduling
计算机科学 6 · 封神冰川 Deified Glacier · 第一个出局 → 双队列 → 队列管理

9.1 队列数据结构概述

Overview of the Queue Data Structure

封神冰川上,你的英雄被冰封住动弹不得,无法直接指挥部队。但没关系——你的军队学会了用队列(queue)自我组织!所有士兵、农民、工人排成整齐的一列,谁先排进队,就谁先被派出去打怪或干活。这一单元由三个关卡组成,你将系统掌握计算机科学中最重要的基础数据结构之一:队列(queue),以及它"先来后到、公平有序"的处事之道。

本单元主线:理解队列是一种先进先出(FIFO)的数据结构;掌握三大操作——enqueue(入队,用 append)、dequeue(出队,用 pop(0))、peek(查看队首但不移除);学会用列表实现队列并在取元素前检查是否为空;根据单位状态(idle)决定是否入队;按队列顺序分配任务;并在每次操作后正确更新队列。

9.1.1 什么是队列(What Is a Queue)

9.1.1.1 一头进、一头出的集合 A collection you add to one end and remove from the other

队列是一种线性集合:元素总是从队尾(back)加入,从队首(front)取出。你唯一能直接取到的,永远是"最早加进来"的那个元素。这和上一单元学的选择排序里的数组不同——队列有严格的进出方向,不能随便从中间插队或取走。

🧊 就像冰川脚下的滑雪缆车排队
游客们在缆车入口排成一队。先到的人排在最前面(队首),后到的人依次接到队尾。缆车来了,永远是最前面那位先上;新来的人只能乖乖排到最后。没有人能插队——这份"先来先服务"的公平,正是队列的灵魂。

9.1.2 FIFO 先进先出(First In, First Out)

9.1.2.1 谁先进,谁先出 The first one in is the first one out

队列的核心规则叫 FIFO——First In, First Out,"先进先出"。第一个排进队列的元素,也会是第一个被取出的。这与另一种结构"栈(stack)"的"后进先出(LIFO)"正好相反。队列的公平性让它特别适合处理"按到达顺序服务"的场景:任务调度、打印排队、消息队列等。

出队 ← A B C D ← 入队
■ 队首 front(A 先进,先出) ·  ■ 队尾 back(D 最后进,最后出)
队列 Queue(FIFO)

先进先出。像排队买票——先来的先走。本单元的主角。

栈 Stack(LIFO)

后进先出。像叠盘子——最后放上去的最先拿走。作为对比了解。

9.2 入队与出队

Enqueue and Dequeue

9.2.1 enqueue 入队(Adding to the Back)

9.2.1.1 把元素加到队尾 Add an element to the back

入队(enqueue)就是把一个新元素加到队列的末尾。在"第一个出局"关卡里,中士(sergeant)有专门的 enqueue(unit) 命令,把士兵一个个排进队;用列表实现时,入队就是 append。

# 关卡里:中士把士兵逐个入队
for soldier in soldiers:
    sergeant.enqueue(soldier)   # 加到队尾

# 用列表实现时,入队 = append
queue = []
queue.append(1)   # ENQUEUE 1 → [1]
queue.append(2)   # ENQUEUE 2 → [1, 2]

9.2.2 dequeue 出队(Removing from the Front)

9.2.2.1 从队首取走元素 Remove the element at the front

出队(dequeue)就是把队首(最早加入)的元素取出并移除。中士用 dequeue() 派出排在最前的士兵去迎敌;用列表实现时,出队就是 pop(0)——移除并返回索引 0(第一个)的元素。

# 关卡里:中士派出队首士兵
sergeant.dequeue()   # 最先入队的先出去

# 用列表实现时,出队 = pop(0)
first = queue.pop(0)   # DEQUEUE,结果 == 1,队列变 [2]
Python 用 pop(0),不是 shift():有些语言用 shift() 从头部取元素,但 Python 没有 shift()。在 Python 里出队一律用 pop(0)——它移除并返回列表第一个元素,效果等同 FIFO 的 dequeue。pop()(不带参数)取的是最后一个,那是栈的行为,别混用。

9.2.3 peek 查看队首(Peeking at the Front)

9.2.3.1 只看不取 Look without removing

查看队首(peek)是指"看一眼最前面是谁",但不把它取走。在"双队列"关卡里,findNearestFriend() 和 findNearestItem() 返回的最近单位/物品,其实就相当于对两个队列做 peek——你先"偷看"队首是什么,再决定要不要处理它。

# peek:只查看队首,不移除
if queue:
    front = queue[0]   # 看第一个是谁,队列不变
操作作用列表写法队列是否改变
enqueue 入队加到队尾queue.append(x)变长
dequeue 出队取走队首queue.pop(0)变短
peek 查看看队首queue[0]不变
isEmpty 判空是否为空not queue / len==0不变

9.3 用列表实现队列

Implementing a Queue with a List

9.3.1 append 与 pop(0)(The Two Core Methods)

9.3.1.1 一个列表 + 两个方法 = 队列 A list plus two methods makes a queue

队列不需要什么特殊类型——一个普通列表就能实现。约定好"从尾部 append 加入、从头部 pop(0) 取出",这个列表就成了一个 FIFO 队列。"队列管理"关卡里,你就要亲手用列表 queue = [] 来管理一支工人队伍。

列表两端各司其职 "One" "Two" "Three" pop(0) 从这头出 ← append 从这头进
# 用列表完整演示一个队列
queue = []
queue.append("One")   # 入队 → ["One"]
queue.append("Two")   # 入队 → ["One", "Two"]
first = queue.pop(0)    # 出队 → first = "One",队列 = ["Two"]
hero.say(first)          # 说出 "One"

9.3.2 空队列检查(Checking for an Empty Queue)

9.3.2.1 取元素前先确认队里有人 Make sure the queue isn't empty first

从空队列里 pop(0) 会直接报错。所以每次出队或 peek 之前,都要先检查队列非空。在 Python 里,非空列表本身为"真"、空列表为"假",可以直接 if queue: 判断,简洁又安全。

# 先判空,再安全出队
if len(queue) > 0:   # 或直接写 if queue:
    worker = queue.pop(0)
养成"先判空"的习惯:处理任何队列/列表的取头、取尾操作前都先判空,是防御性编程的重要一环。哪怕你觉得"队列此刻肯定有人",加上这层检查也能避免边界情况下的崩溃。

9.4 按状态与顺序调度

Scheduling by Status and Order

9.4.1 idle 决定入队(Enqueue Based on Status)

9.4.1.1 只让"空闲"的单位排队 Only enqueue units that are idle

在"队列管理"关卡里,工人有一个 status 属性。只有当工人空闲(idle)——即没在干活、正等着派活时,才应该把他加入队列。这避免了把正在忙碌的工人重复排进队。判断很简单:if worker.status == "idle": 就入队。

# 遍历工人,空闲的才入队
for worker in workers:
    if worker.status == "idle":
        hero.say(worker + " in")   # 语音命令:让他进队
        queue.append(worker)          # 同步加入我们维护的队列
用状态过滤,避免重复:为什么要判断 idle?因为已经被派去干活的工人状态会变(不再 idle),如果不加这层过滤,每帧都会把所有工人重复入队,队列就乱了。用状态作为"是否需要排队"的开关,是任务调度里常见的模式。

9.4.2 顺序处理任务(Processing Tasks in Order)

9.4.2.1 任务与工人的配对 Matching tasks with workers

队列最经典的用途就是任务分配。把工人看成"资源"、把物品/敌人看成"任务":当有任务到来(如出现一个物品),就从工人队列里 pop(0) 出队首那位——他排队最久,理应最先被派工。在"双队列"关卡里,则是同时 peek 两个队列(单位队列、物品队列),看队首的组合是否匹配(如药水配士兵、剑配农民),匹配就处理,不匹配就喊"Next"把它挪到队尾。

任务到来 → 队首工人出队接单 工人A 工人B 工人C → 任务来了 A 排最久 → A 先出队接任务

9.5 循环控制队列更新

Controlling Queue Updates in a Loop

9.5.1 每次操作后更新(Update After Each Operation)

9.5.1.1 一次只放一个走,及时更新队列 Let one out at a time, keep the queue in sync

"队列管理"关卡特别强调:每次只让第一名工人离队,并在每个命令之后保证队列被正确更新。也就是说,喊完 "Name out" 派走队首后,必须立刻 pop(0) 把他从队列里移除,让下一位递补成新队首。若不同步更新,队列里的顺序就会与实际情况脱节,导致重复派工或漏派。

# 有任务且队列非空时,派出队首并同步移除
item = hero.findNearestItem()
if item and len(queue) > 0:
    hero.say(queue[0] + " out")   # 喊队首出列
    queue.pop(0)                    # 同步从队列移除,下一位递补
空闲工人入队 有任务? 队首出队接任务 循环

9.6 实战关卡精讲

Level Strategy & Analysis

这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。

第 18 关 · 第一个出局

🧊 第一个出局 —— 用队列组织士兵与农民

扣哒世界 第一个出局关卡 - koudashijie.com
图2:第一个出局——英雄被冰封,靠中士 enqueue 把士兵排队、敌人来袭时 dequeue 依次派出,实践最基本的 FIFO

策略:先把队排好,再按需依次放出

英雄被冰封无法直接指挥,但中士(sergeant)能 enqueue 入队、dequeue 出队。策略分两阶段:准备阶段——先把所有士兵(和农民)逐个 enqueue 排进队列;作战阶段——侦察兵(scout)发现敌人时,用 dequeue 把队首士兵一个个派出去迎敌;等敌人清空后,再依次放出农民。这就是最纯粹的 FIFO 应用。

思路拆解:入队准备 + 按信号出队

先用 for 循环把 soldiers 逐个 sergeant.enqueue()。然后在循环里用 scout.findNearestEnemy() 侦测:一旦发现敌人,就 dequeue 放出队首士兵应战。关键在于顺序——最先入队的士兵最先出战,公平且有序。

# 关键片段:准备阶段——士兵逐个入队
for soldier in soldiers:
    sergeant.enqueue(soldier)

# 关键片段:敌人来袭时,派出队首士兵
enemy = scout.findNearestEnemy()
if enemy:
    sergeant.dequeue()   # 最先入队者最先出战
enqueue/dequeue 的本质:本关的 enqueue/dequeue 是中士的特殊命令,帮你直观感受队列的两端操作。它们背后的逻辑,和用列表 append/pop(0) 完全一致——加到队尾、从队首取走。理解了这一关,后面两关自己用列表实现队列就顺理成章。
第 19 关 · 双队列

❄️ 双队列 —— peek 两个队列做任务配对

扣哒世界 双队列关卡 - koudashijie.com
图3:双队列——单位队列与物品队列并行,peek 两队队首判断是否匹配(药水配士兵、剑配农民),不匹配就喊 Next

策略:同时看两个队首,能配对就处理

这一关有两个队列:单位队列和物品队列。用 findNearestFriend() 和 findNearestItem() 分别 peek(偷看)两队的队首。规则是:如果队首物品是药水(potion)且队首单位是士兵(soldier),或者队首物品是剑(sword)且队首单位是农民(peasant),就匹配成功——说出单位名字处理它;否则喊 "Next" 把队首挪到队尾,换下一个来试。

思路拆解:先判空,再按 type 配对

每轮先 peek 出 item 和 unit,若任一为空(not item or not unit)就 continue 跳过——这就是"取元素前先判空"。拿到后用 type 属性判断配对关系,用 if / elif / else 分三种情况处理。

# 关键片段:peek 两队队首,按类型配对
item = hero.findNearestItem()     # peek 物品队列
unit = hero.findNearestFriend()   # peek 单位队列
if not item or not unit:
    continue                          # 判空保护
if item.type == "potion" and unit.type == "soldier":
    hero.say(unit.id)                 # 药水配士兵
elif item.type == "sword" and unit.type == "peasant":
    hero.say(unit.id)                 # 剑配农民
else:
    hero.say("Next")              # 不匹配,挪到队尾
"Next" 的作用:喊 "Next" 相当于对队列做一次"轮转"——把当前队首送到队尾,让下一个元素成为新队首。这样即使当前队首暂时无法配对,也不会卡死,队列会不断轮换直到找到能匹配的组合。这体现了队列在任务调度中的灵活性。
墙会挡住视线:关卡提示墙壁会阻挡感知(连 Twilight 眼镜也挡),所以 findNearest 只能看到当前可见的队首。这恰好符合队列"只能访问队首"的特性——你不需要看到全部,只处理最前面的即可。
第 20 关 · 队列管理

🏔️ 队列管理 —— 亲手用列表维护工人队列

扣哒世界 队列管理关卡 - koudashijie.com
图4:队列管理——用 queue=[] 自建队列,空闲工人喊 in 入队,有任务时喊队首 out 并 pop(0) 移除,每步保持队列同步

策略:自建队列,in 入队、out 出队,每步同步

这是本单元的综合关:你要亲手用一个列表 queue = [] 管理工人。用语音命令控制——喊 "名字 in" 让工人进队,喊 "名字 out" 让工人出队。策略:遍历所有工人,空闲(idle)的就喊 in 并 append 入队;当有任务(物品)且队列非空时,喊队首 out 并 pop(0) 移除,让下一位递补。

思路拆解一:空闲工人入队

遍历 workers,用 worker.status == "idle" 筛出空闲的,先喊 hero.say(worker + " in") 发出入队命令,再把他 append 进我们维护的 queue,保持"语音命令"与"程序里的队列"两边一致。

# 关键片段:空闲工人入队
for worker in workers:
    if worker.status == IDLE:          # IDLE = "idle"
        hero.say(worker + " " + PHRASE_IN)   # "名字 in"
        queue.append(worker)              # 同步入队

思路拆解二:有任务就派队首出队

检查是否有任务(item 存在)且队列非空。两个条件都满足,才喊队首工人 out,并立刻 pop(0) 把他移出队列。"先判空再取"和"每步同步更新"是本关得分的关键。

# 关键片段:有任务且队列非空时,派出队首
item = hero.findNearestItem()
if item and len(queue) > 0:
    hero.say(queue[0] + " " + PHRASE_OUT)   # "队首名字 out"
    queue.pop(0)                              # 同步移除队首
Python 用 pop(0) 而非 shift():关卡明确提示——在 Python 里要用 array.pop(0) 代替 array.shift(),因为 Python 没有 shift()。别照搬其他语言的写法。同时切记只让第一名工人离队(pop(0) 每次只移一个),并在每个命令后同步更新队列。
用常量让代码更清晰:关卡预定义了 PHRASE_IN = "in"、PHRASE_OUT = "out"、IDLE = "idle" 这些常量。用它们代替裸字符串,既能避免拼写错误,也让代码意图一目了然——这是良好的编程习惯。

9.7 常见错误

Common Mistakes

9.7.1 新手最容易踩的坑(Pitfalls for Beginners)

9.7.1.1 队列操作的典型问题 Typical issues with queue operations

扣哒世界 队列操作常见错误速查 - koudashijie.com
图5:用错 pop()、忘记判空、不同步更新队列、没按状态过滤,是本单元最常见的失误
易错点清单:
  • 出队用错方法——FIFO 出队要 pop(0)(取队首);写成 pop() 会取队尾,变成了栈的行为。
  • 用了 shift()——Python 没有 shift(),出队一律用 pop(0)。
  • 取元素前不判空——从空队列 pop(0) 会报错;出队/peek 前务必 if queue: 检查。
  • 队列不同步更新——喊了 out 却忘 pop(0),队首没移除,会重复派同一个人。
  • 不按 idle 过滤——不判断 status == "idle" 就入队,会把忙碌工人重复排进队列。
  • 一次放走多个——本关要求每命令只让第一名离队,pop(0) 每次只移一个,别在一步里连出多个。
  • peek 和 dequeue 混淆——只想"看队首"用 queue[0](不移除);要"取走"才用 pop(0)。
  • 入队用错端——入队要 append(加到队尾);若加到队首会破坏 FIFO 顺序。

本单元小结 ✨

恭喜你掌握了计算机科学中最实用的基础数据结构之一——队列!在封神冰川的这三关里,你理解了队列的先进先出(FIFO)特性;掌握了三大核心操作——enqueue 入队(append 到队尾)、dequeue 出队(pop(0) 取队首)、peek 查看队首(queue[0] 不移除);学会了用列表实现队列并在取元素前判空保护;懂得根据 status == "idle" 决定是否入队、按队列顺序做任务分配;也牢记了每次操作后必须同步更新队列。从"第一个出局"的基础 FIFO,到"双队列"的 peek 配对,再到"队列管理"的亲手实现,你已经能用队列优雅地组织复杂的工作流。来回顾一下核心概念:

核心概念关键点对应写法
FIFO 特性先进先出First In First Out
enqueue 入队加到队尾queue.append(x)
dequeue 出队取走队首queue.pop(0)
peek 查看看队首不移除queue[0]
空队列检查取前先判空if queue: / len>0
按状态入队空闲才排队if worker.status=="idle"
任务分配队首先接单配对 / say("Next")
同步更新每步保持一致say out → pop(0)

懂得让资源与任务"先来先服务、有序流转"——队列这份公平与秩序的智慧,会成为你构建复杂系统的可靠基石。
封神冰川的队列试炼圆满完成,向着更深的数据结构世界,继续前行!🧊❄️📋

扣哒世界 编程知识 · 计算机科学 6(封神冰川):第九单元

扣哒世界教研团队