封神冰川上,你的英雄被冰封住动弹不得,无法直接指挥部队。但没关系——你的军队学会了用队列(queue)自我组织!所有士兵、农民、工人排成整齐的一列,谁先排进队,就谁先被派出去打怪或干活。这一单元由三个关卡组成,你将系统掌握计算机科学中最重要的基础数据结构之一:队列(queue),以及它"先来后到、公平有序"的处事之道。
enqueue(入队,用 append)、dequeue(出队,用 pop(0))、peek(查看队首但不移除);学会用列表实现队列并在取元素前检查是否为空;根据单位状态(idle)决定是否入队;按队列顺序分配任务;并在每次操作后正确更新队列。
队列是一种线性集合:元素总是从队尾(back)加入,从队首(front)取出。你唯一能直接取到的,永远是"最早加进来"的那个元素。这和上一单元学的选择排序里的数组不同——队列有严格的进出方向,不能随便从中间插队或取走。
队列的核心规则叫 FIFO——First In, First Out,"先进先出"。第一个排进队列的元素,也会是第一个被取出的。这与另一种结构"栈(stack)"的"后进先出(LIFO)"正好相反。队列的公平性让它特别适合处理"按到达顺序服务"的场景:任务调度、打印排队、消息队列等。
先进先出。像排队买票——先来的先走。本单元的主角。
后进先出。像叠盘子——最后放上去的最先拿走。作为对比了解。
入队(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]
出队(dequeue)就是把队首(最早加入)的元素取出并移除。中士用 dequeue() 派出排在最前的士兵去迎敌;用列表实现时,出队就是 pop(0)——移除并返回索引 0(第一个)的元素。
# 关卡里:中士派出队首士兵 sergeant.dequeue() # 最先入队的先出去 # 用列表实现时,出队 = pop(0) first = queue.pop(0) # DEQUEUE,结果 == 1,队列变 [2]
shift() 从头部取元素,但 Python 没有 shift()。在 Python 里出队一律用 pop(0)——它移除并返回列表第一个元素,效果等同 FIFO 的 dequeue。pop()(不带参数)取的是最后一个,那是栈的行为,别混用。
查看队首(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 | 不变 |
队列不需要什么特殊类型——一个普通列表就能实现。约定好"从尾部 append 加入、从头部 pop(0) 取出",这个列表就成了一个 FIFO 队列。"队列管理"关卡里,你就要亲手用列表 queue = [] 来管理一支工人队伍。
# 用列表完整演示一个队列 queue = [] queue.append("One") # 入队 → ["One"] queue.append("Two") # 入队 → ["One", "Two"] first = queue.pop(0) # 出队 → first = "One",队列 = ["Two"] hero.say(first) # 说出 "One"
从空队列里 pop(0) 会直接报错。所以每次出队或 peek 之前,都要先检查队列非空。在 Python 里,非空列表本身为"真"、空列表为"假",可以直接 if queue: 判断,简洁又安全。
# 先判空,再安全出队 if len(queue) > 0: # 或直接写 if queue: worker = queue.pop(0)
在"队列管理"关卡里,工人有一个 status 属性。只有当工人空闲(idle)——即没在干活、正等着派活时,才应该把他加入队列。这避免了把正在忙碌的工人重复排进队。判断很简单:if worker.status == "idle": 就入队。
# 遍历工人,空闲的才入队 for worker in workers: if worker.status == "idle": hero.say(worker + " in") # 语音命令:让他进队 queue.append(worker) # 同步加入我们维护的队列
队列最经典的用途就是任务分配。把工人看成"资源"、把物品/敌人看成"任务":当有任务到来(如出现一个物品),就从工人队列里 pop(0) 出队首那位——他排队最久,理应最先被派工。在"双队列"关卡里,则是同时 peek 两个队列(单位队列、物品队列),看队首的组合是否匹配(如药水配士兵、剑配农民),匹配就处理,不匹配就喊"Next"把它挪到队尾。
"队列管理"关卡特别强调:每次只让第一名工人离队,并在每个命令之后保证队列被正确更新。也就是说,喊完 "Name out" 派走队首后,必须立刻 pop(0) 把他从队列里移除,让下一位递补成新队首。若不同步更新,队列里的顺序就会与实际情况脱节,导致重复派工或漏派。
# 有任务且队列非空时,派出队首并同步移除 item = hero.findNearestItem() if item and len(queue) > 0: hero.say(queue[0] + " out") # 喊队首出列 queue.pop(0) # 同步从队列移除,下一位递补
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
英雄被冰封无法直接指挥,但中士(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 是中士的特殊命令,帮你直观感受队列的两端操作。它们背后的逻辑,和用列表 append/pop(0) 完全一致——加到队尾、从队首取走。理解了这一关,后面两关自己用列表实现队列就顺理成章。
这一关有两个队列:单位队列和物品队列。用 findNearestFriend() 和 findNearestItem() 分别 peek(偷看)两队的队首。规则是:如果队首物品是药水(potion)且队首单位是士兵(soldier),或者队首物品是剑(sword)且队首单位是农民(peasant),就匹配成功——说出单位名字处理它;否则喊 "Next" 把队首挪到队尾,换下一个来试。
每轮先 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" 相当于对队列做一次"轮转"——把当前队首送到队尾,让下一个元素成为新队首。这样即使当前队首暂时无法配对,也不会卡死,队列会不断轮换直到找到能匹配的组合。这体现了队列在任务调度中的灵活性。
findNearest 只能看到当前可见的队首。这恰好符合队列"只能访问队首"的特性——你不需要看到全部,只处理最前面的即可。
这是本单元的综合关:你要亲手用一个列表 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) # 同步移除队首
array.pop(0) 代替 array.shift(),因为 Python 没有 shift()。别照搬其他语言的写法。同时切记只让第一名工人离队(pop(0) 每次只移一个),并在每个命令后同步更新队列。
PHRASE_IN = "in"、PHRASE_OUT = "out"、IDLE = "idle" 这些常量。用它们代替裸字符串,既能避免拼写错误,也让代码意图一目了然——这是良好的编程习惯。
pop(0)(取队首);写成 pop() 会取队尾,变成了栈的行为。shift(),出队一律用 pop(0)。pop(0) 会报错;出队/peek 前务必 if queue: 检查。pop(0),队首没移除,会重复派同一个人。status == "idle" 就入队,会把忙碌工人重复排进队列。pop(0) 每次只移一个,别在一步里连出多个。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(封神冰川):第九单元
扣哒世界教研团队