封神冰川的城门前,站着 8 名刚入伍、尚未受训的新兵,队伍杂乱无章。你的任务是把他们按身高从矮到高、从左到右排成一列。在游戏里,士兵的身高与他的 health(生命值)属性成正比——所以"按身高排序"其实就是"按血量排序"。要让程序自动完成这件事,你需要掌握一种经典而直观的排序算法:选择排序(selection sort)。
minHealthUnit);用 command 把选出的最弱单位移动到指定标记位置;每放好一个就更新未排序名单;用 while 循环重复直到所有人各就各位。
排序,就是把一组杂乱的数据按某种规则(从小到大、从高到矮…)重新排列成有序的一列。排序是计算机科学最基础也最常用的操作之一——有序的数据才能用上一单元的二分查找,才能快速找出最大最小、才能整齐地展示。本单元的目标数据就是 8 名士兵,排序依据是他们的 health。
选择排序的名字里"选择"二字就是关键:每一轮都在剩下的未排序数据里选出最小的一个,把它放到已排好队列的末尾。第一轮选出全场最矮的,放第 1 位;第二轮从剩下的里选最矮的,放第 2 位……如此往复,队列一个接一个地变长,直到所有人都被选完。
# 选择排序的通用骨架(以数字为例) unsorted = [10, 12, 3, 1, 2, 6] result = [] while len(unsorted): smallest = min(unsorted) # 找最小 result.append(smallest) # 放到结果末尾 unsorted.remove(smallest) # 从未排序里移除
虽然 Python 有内置的 min(),但对一群士兵对象我们要按 health 比较,所以自己写一个求最小值的函数。方法是经典的求最小值算法:先设一个大得离谱的初始值 minHealth = 9001,遍历每个单位,谁的血量比当前记录更低,就更新记录并记住这个单位。遍历结束,记住的就是全场最弱者。
关卡要求你补全 minHealthUnit(units),它接收一个单位列表,返回其中 health 最低的那个。这就是上面求最小值算法的实现——它是整个排序的"引擎",每一轮都靠它挑出下一个该归位的士兵。
# 返回列表中 health 最低(最矮)的单位 def minHealthUnit(units): weakest = None minHealth = 9001 # 故意设很大,方便被刷新 for unit in units: if unit.health < minHealth: minHealth = unit.health weakest = unit # 记住当前最弱者 return weakest
minHealth 必须比任何单位的血量都大,这样第一个士兵就一定能"打赢"它、成功刷新记录。9001 只是一个足够大的数字(也是个梗);只要比所有可能的血量都大即可。反过来求最大值时,初始值要设得很小(如 0)。
队形里有一排标记(mark),第 0 个、第 1 个…依次向右排开。关卡提供了 markPos(i) 函数:给它一个序号 i,它返回第 i 个标记的坐标——本质是 x = 12 + i*8, y = 12,即每往右一位 x 加 8。我们用一个计数器 markIndex 记录"下一个该填的位置",从 0 开始。
# 关卡提供:返回第 i 个标记的坐标 def markPos(i): return {"x": 12 + i * 8, "y": 12} markIndex = 0 # 下一个要填的位置序号
选出本轮最弱的士兵后,用 hero.command(weakestRecruit, "move", markPos(markIndex)) 命令他走到第 markIndex 个标记处。归位之后,把 markIndex 加 1,让下一轮的最弱者去填下一个位置。这样最矮的排最左、次矮的排其次……队形自然从矮到高。
# 找到最弱者,命令他去当前标记,再让序号前进 weakestRecruit = minHealthUnit(recruits) hero.command(weakestRecruit, "move", markPos(markIndex)) markIndex += 1 # 下一位往右挪一格
普通选择排序要手动 remove 已排好的元素,但这一关有个巧妙之处:findUnplacedSoldiers() 只返回还站在起点区、状态为 idle(发呆)的新兵。一旦你命令某个士兵移动,他就离开起点、不再 idle,下次查询名单时自动被排除。所以我们不需要手动移除,只要每轮重新调用 findUnplacedSoldiers() 刷新名单即可。
# 只返回还在起点、发呆(idle)的新兵 def findUnplacedSoldiers(): friends = hero.findFriends() result = [] for friend in friends: if 18 < friend.pos.y and friend.pos.y < 30 and friend.action == "idle": result.append(friend) return result
每轮把一个士兵送去归位后,在循环末尾重新赋值 recruits = findUnplacedSoldiers()。因为刚归位那位已不再 idle,新名单会比上一轮少一个人。这样 recruits 每轮缩短,直到变成空列表——排序也就完成了。
recruits 会一直是最初那一整批人,while 循环永远不空、也永远选到同一批最弱者反复命令——排序陷入死循环。刷新名单是让循环能正常结束的关键。
while len(recruits): 是这道关卡的主循环条件。在 Python 里,非空列表的长度是正数(真),空列表长度是 0(假)。所以只要 recruits 里还有人,循环就继续;一旦所有人都归位、名单变空,len(recruits) 为 0,循环自动停止。这是一种优雅的"处理到没有为止"写法。
# 主循环骨架:还有未归位的新兵就继续 recruits = findUnplacedSoldiers() markIndex = 0 while len(recruits): weakest = minHealthUnit(recruits) # 选最弱 hero.command(weakest, "move", markPos(markIndex)) # 归位 markIndex += 1 # 序号前进 recruits = findUnplacedSoldiers() # 刷新名单
选择排序简单直观,但效率不高。对 n 个元素,第一轮要在 n 个里找最小,第二轮在 n-1 个里找,第三轮 n-2 个……总比较次数是 n + (n-1) + ... + 2 + 1 = n(n+1)/2,属于 O(n²) 级别。数据翻倍,耗时约变四倍。它适合数据量小、追求代码简单的场景;处理海量数据时,会换用更快的归并排序、快速排序等。
| 元素个数 n | 大致比较次数 n(n+1)/2 |
|---|---|
| 8(本关) | 36 |
| 100 | 5050 |
| 1000 | 约 50 万 |
这一节聚焦关卡的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
身高与 health 成正比,所以"按身高排"就是"按血量排"。策略就是选择排序:每一轮从还没归位的新兵里挑出血量最低(最矮)的那个,命令他走到最左边还空着的标记处,然后处理下一位。因为最矮的先占最左位、次矮的占其次……最终队形自然从矮到高、从左到右。
排序的"引擎"是 minHealthUnit。用求最小值算法遍历传入的士兵列表,初始 minHealth = 9001,谁血量更低就更新记录,最后返回最弱的那个单位。这个函数你在前面单元其实已经练过类似的(寻找最低血量友军),迁移过来即可。
# 关键片段:返回 health 最低的单位 def minHealthUnit(units): weakest = None minHealth = 9001 for unit in units: if unit.health < minHealth: minHealth = unit.health weakest = unit return weakest
主循环 while len(recruits): 里每轮做四件事,顺序不能乱:① 用 minHealthUnit(recruits) 找出本轮最弱者;② 用 command + markPos(markIndex) 命令他去当前标记;③ markIndex += 1 让位置序号前进;④ 重新 findUnplacedSoldiers() 刷新名单。因为归位的士兵不再 idle,会自动从新名单里消失,名单每轮变短,直到为空、循环结束。
# 关键片段:选择排序主循环的四个动作 while len(recruits): weakestRecruit = minHealthUnit(recruits) # ① 选最弱 hero.command(weakestRecruit, "move", markPos(markIndex)) # ② 归位 markIndex += 1 # ③ 序号+1 recruits = findUnplacedSoldiers() # ④ 刷新名单
unsorted.remove(smallest) 把已排好的移除。但本关里,被命令移动的士兵状态从 idle 变成 move,findUnplacedSoldiers() 就不再把他算进去——相当于"自动移除"。所以每轮只需重新查询名单,无需手动删除元素。
markIndex += 1,会导致士兵挤到同一个标记、或反复命令同一个人。归位与序号自增要成对出现。
findUnplacedSoldiers(),名单永不变空,陷入死循环。markIndex += 1,所有士兵都被送到同一个标记,挤成一团。minHealth 要设很大(如 9001),设成 0 就永远没人比它更小,返回 None。weakest,调用方拿到 None,命令移动会出错。unit.health < minHealth;写成 > 就变成找最强了。恭喜你把一群散兵游勇整编成了一列整齐的方阵!在封神冰川的这一单元,你掌握了经典的选择排序算法——每一轮都从未排序的数据里选出最小的一个,放到有序队列的下一个位置。你学会用遍历求最小值的方式写出 minHealthUnit 函数(初值设大、遇更小就刷新);用 markPos(markIndex) 配合 command 把选出的最弱士兵送到指定标记;懂得每轮重新 findUnplacedSoldiers() 更新未排序名单(归位者自动消失);并用 while len(recruits): 循环到名单为空、排序完成。你也了解了它 O(n²) 的复杂度与适用场景。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应写法 |
|---|---|---|
| 选择排序思想 | 每轮选最小放队尾 | pick min → append |
| 求最小值 | 初值设大,遇小刷新 | minHealth = 9001 |
| 最弱者函数 | 返回 health 最低单位 | minHealthUnit(units) |
| 按序号定位 | 算出标记坐标 | markPos(markIndex) |
| 命令归位 | 送到当前标记 | command(u, "move", ...) |
| 序号前进 | 填下一个位置 | markIndex += 1 |
| 更新名单 | 归位者自动消失 | recruits = findUnplaced...() |
| 循环终止 | 名单空则结束 | while len(recruits) |
懂得反复"择出最优、逐一归位",再杂乱的数据也能被你整理得井然有序——这就是排序算法的力量。
封神冰川的整编大功告成,向着数据结构的更高峰,继续攀登!🧊❄️📊
扣哒世界 编程知识 · 计算机科学 6(封神冰川):第八单元
扣哒世界教研团队