上一单元你学会了指挥一支队伍、挑选最优目标。这一单元我们要研究一个看似简单、却处处有坑的主题——反转(reversal)。有时候我们手上的数据顺序正好和需要的相反:巫师只能把敌人从最强到最弱排序,可我们必须从最弱到最强逐个击破;术士给每个弓箭手克隆了一个"名字倒着念"的邪恶分身,我们得靠把字符串反转来找出谁是谁的对头。学会反转,就是学会"倒着看世界"。
for i in range(len-1, -1, -1) 从后往前倒序遍历列表;用"从末尾逐字符拼接"的算法反转一个字符串;判断"一个字符串反转后是否等于另一个字符串"来做反向匹配;用嵌套循环遍历所有"友军×敌军"的配对组合;并用 break 在找到答案后提前终止循环,避免无谓的计算。
编程里经常遇到这种局面:你能拿到的数据,顺序和你需要的正好相反。在"雪人食人族"关卡中,巫师的排序法术只能生成由强到弱的雪人列表,但英雄吞噬生命力的机制要求由弱到强逐个击败。与其请巫师重排(他做不到),不如我们自己倒着读这个列表——这就是倒序遍历的用武之地。
"archer" → "rehcra")。前者关心"怎么读",后者关心"造出什么"。
要倒着读列表,得先知道"最后一个元素在哪儿"。Python 的索引从 0 开始,所以一个长度为 n 的列表,第一个元素索引是 0,最后一个元素索引是 n - 1(也就是 len(array) - 1)。倒序遍历就是从这个最大索引出发,一步步减到 0。
array[len(array)] 会越界报错(IndexError),因为长度为 4 的列表最大合法索引是 3,而不是 4。倒序起点永远是 len(array) - 1,牢记这个"减一"。
正序遍历我们常写 for i in range(len(array)),它默认从 0 开始、步长为 1、走到 len-1。要倒序,就得用到 range 的完整三参数形式:range(起点, 终点, 步长)。倒序遍历的写法是这个单元最核心的一行代码:
# 倒序遍历:从最后一个元素走到第一个 array = [1, 2, 3] for i in range(len(array) - 1, -1, -1): hero.say(array[i]) # 依次说出 3, 2, 1
这是倒序遍历最容易搞错的地方。range 的终点值是"取不到"的(左闭右开)。我们希望 i 一直减到 0 并且包含 0(第 0 个元素也要访问),所以终点必须再往下一格写成 -1。如果写成 range(len-1, 0, -1),循环会在 i = 1 时停下,漏掉第 0 个元素。
| 参数 | 取值 | 含义 |
|---|---|---|
| 起点 | len(array) - 1 | 从最后一个元素的索引开始 |
| 终点 | -1 | 减到 0 之后再走一步才停,保证包含索引 0 |
| 步长 | -1 | 每次索引减 1,实现"往回走" |
倒序遍历也能用 while 循环实现,本质完全一样——手动设一个索引,每轮减 1,直到它小于 0。理解这个写法能帮你看清 range 背后到底发生了什么。
for i in range(len(array)-1, -1, -1): hero.say(array[i])
index = len(array) - 1 while index >= 0: hero.say(array[index]) index -= 1
while index >= 0 和 while index > -1 意思完全相同。用 while 时千万别忘了循环体末尾的 index -= 1,否则索引永远不变,会变成死循环。
反转字符串的秘诀,是意识到字符串和数组本质上是一回事——都是一串按索引排列的元素。数组的元素是任意值,字符串的元素则是单个字符。所以 "archer"[0] 是 "a","archer"[5] 是 "r",len("archer") 是 6。倒序遍历列表的技巧,可以原样搬到字符串上。
关卡"拓地"给出了标准的字符串反转算法。思路很朴素:准备一个空字符串,然后从原字符串的最后一个字符开始,一个个把它们拼接到新字符串的末尾。原来最后的字符第一个被拼进去,就自然排到了新串的最前面——反转就完成了。
# 反转一个字符串:从末尾往前逐字符拼接 def reverseString(text): result = "" # 1. 空字符串 for i in range(len(text)-1, -1, -1): # 2. 从末尾开始倒序 result += text[i] # 3. 把字符拼到后面 return result
result += text[i] 等价于 result = result + text[i],意思是"把新字符接到已有结果的末尾"。因为我们是倒着取字符、顺着往后接,所以整体效果就是反转。
"拓地"关卡真正要判断的,是两个单位的名字是否互为反向——比如 "tim" 和 "mit"。做法非常直接:把第一个名字反转,看反转结果是否恰好等于第二个名字。这就是关卡里的 areAntipodes(对映体判断)函数。
# 判断两个单位的 id 是否互为反转 def areAntipodes(unit1, unit2): reversed1 = "" for i in range(len(unit1.id)-1, -1, -1): reversed1 += unit1.id[i] return reversed1 == unit2.id # 反转后相等 → 是对映体
return reversed1 == unit2.id 会直接把比较的结果(True 或 False)返回。不需要写成 if ... return True else return False,那样是多余的——比较表达式本身就是一个布尔值。
要给每个弓箭手找到它的反向克隆,我们不知道谁配谁,只能把每个友军和每个敌军都比一遍。这需要嵌套循环:外层循环遍历每个友军,内层循环遍历每个敌军,在内层里检查"这一对是不是互为反转"。如果是,就命令这个友军移向对应的敌人。
friends = hero.findFriends() enemies = hero.findEnemies() for friend in friends: # 外层:每个友军 for enemy in enemies: # 内层:每个敌军 if areAntipodes(friend, enemy): hero.command(friend, "move", enemy.pos) break # 找到对手就不用再比了
嵌套循环的一个重要直觉:总的比较次数是两层循环长度的乘积。如果有 5 个友军、5 个敌军,内层就会执行 5×5 = 25 次。友军和敌军越多,比较次数增长得越快。这也是为什么下面要讲的 break 很有价值——它能在找到答案后省下剩余的无用比较。
areAntipodes 内部本身还有一个反转字符串的循环。所以这里其实是"三层"计算:外层友军、内层敌军、最内层反转字符。理解这一点,你就能明白代码到底做了多少工作。
每个弓箭手只有一个反向对手。一旦在内层循环里找到了它、并下达了移动命令,就没必要再把剩下的敌人比完了。break 语句会立即跳出它所在的那一层循环,让外层循环去处理下一个友军。这既符合逻辑,又省下计算。
for enemy in enemies: if areAntipodes(friend, enemy): hero.command(friend, "move", enemy.pos) break # 已找到对手,跳出内层循环
break 做的正是这件事——目标达成,立刻退出循环。
break:彻底跳出整个循环,后面的元素一个都不再看。
continue:只跳过当前这一轮,循环仍会继续处理后面的元素。
break 只会跳出内层,外层循环照常继续。所以上面的代码里,一个友军找到对手后 break,外层会立刻接着处理下一个友军——这正是我们想要的。
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关的核心机制是:英雄击败雪人时会吞噬它的生命力,所以必须从最弱的开始打,越打越强,才能应付后面更强的雪人。麻烦在于——巫师的排序法术只能把雪人排成从最强到最弱的列表。既然列表方向反了,我们就倒序遍历这个列表:从最后一个(最弱的)打到第一个(最强的)。
第一步,拿到排好序的列表:找到巫师朋友,调用它的 findEnemies() 得到"由强到弱"的雪人列表 yetis。第二步,倒序遍历:用 for i in range(len(yetis)-1, -1, -1),让索引 i 从最后往前走,这样先访问到的就是最弱的雪人。第三步,逐个击败:对每个雪人反复攻击,直到它的生命值归零,再进入下一个。
# 关键片段:倒序遍历,从最弱雪人打起 wizard = hero.findNearest(hero.findFriends()) yetis = wizard.findEnemies() # 由强到弱的列表 for i in range(len(yetis)-1, -1, -1): # 倒序 → 先弱后强 while yetis[i].health > 0: hero.attack(yetis[i]) # 打到这个雪人倒下
while yetis[i].health > 0 内层循环,保证对当前这个雪人持续攻击、直到它彻底倒下,再由外层 for 推进到下一个更强的雪人。这是"外层选目标、内层打到死"的经典结构。
range(len(yetis)-1, -1, -1)。起点写成 len(yetis)-1 才是最弱雪人;步长 -1 才能往回走。任何一处写错,就会先打到强雪人而被反杀。
术士用"克隆"法术造出了我方弓箭手的邪恶镜像——每个克隆的名字都是某个友军名字的反向拼写(如友军 tim 对应克隆 mit)。规则很微妙:如果弓箭手接触到自己正确的对手,克隆就会消失;但如果碰到错误的克隆或去攻击它,克隆就会开始战斗。所以我们必须精确配对,只让每个友军去接触它真正的反向对手。等所有克隆湮灭后,再一起攻击术士。
本关的算法结构非常有层次。最内层是反转字符串:areAntipodes 函数把 unit1.id 倒序拼接成 reversed1,再判断 reversed1 == unit2.id,返回它们是否互为反向。中间层是嵌套循环配对:外层遍历每个友军、内层遍历每个敌军,对每一对调用 areAntipodes。最外层是收尾:所有克隆消失后攻击术士。
# 关键片段一:反转 id 判断是否互为对映体 def areAntipodes(unit1, unit2): reversed1 = "" for i in range(len(unit1.id)-1, -1, -1): reversed1 += unit1.id[i] return reversed1 == unit2.id
# 关键片段二:嵌套循环为每个友军找到对手 for friend in friends: for enemy in enemies: if areAntipodes(friend, enemy): hero.command(friend, "move", enemy.pos) break # 找到唯一对手即停
克隆全部湮灭后,场上只剩术士。用一个 while True 循环持续查找最近敌人,若它确实是术士(type == "warlock"),就指挥所有弓箭手一起攻击、英雄也一同出手。集火才能快速解决这个能"感受侵略"的对手。
# 关键片段三:所有克隆消失后集火术士 while True: nearest = hero.findNearest(hero.findEnemies()) if nearest and nearest.type == "warlock": for friend in friends: hero.command(friend, "attack", nearest) hero.attack(nearest)
move 命令。这正是精确匹配比"就近处理"更重要的场景。
len(array) - 1,直接写 len(array) 会索引越界报错。range(len-1, 0, -1) 会漏掉第 0 个元素(最强雪人或名字首字符),终点必须是 -1。range(len-1, -1, 1) 步长为正、起点又大于终点,循环一次都不执行。倒序步长必须是 -1。result = "",否则拼接时会因变量未定义而报错。result += text[i],取的是当前索引的字符,不要写死成某个固定下标。break 要放在"找到对手并下命令之后",且只跳内层;放错会导致提前中断或永不停止。while 写倒序时,循环体末尾必须有 index -= 1,否则死循环。areAntipodes 确认再移动,碰错克隆会激活它反被攻击。这一单元你掌握了"倒着看世界"的一整套本领!你学会了用 for i in range(len-1, -1, -1) 倒序遍历列表——起点是最后索引、终点是 -1、步长是 -1,缺一不可;理解了字符串本质上就是字符序列,从而用"从末尾逐字符拼接"的五步算法反转任意字符串;会用"反转再用 == 比较"来判断两个名字是否互为反向;用嵌套循环遍历所有"友军×敌军"配对,并在找到对手后用 break 提前终止内层循环。在"雪人食人族"里你倒序击败雪人,在"拓地"里你为每个弓箭手精确匹配了反向克隆。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| 倒序遍历列表 | range(len-1, -1, -1) | 雪人食人族 |
| while 倒序 | 索引从 len-1 起,每轮 -1 | 雪人食人族 |
| 外层选目标内层打 | for 选雪人,while 打到死 | 雪人食人族 |
| 字符串反转 | 空串起步,末尾逐字符拼接 | 拓地 |
| 反转后判等 | reversed1 == unit2.id | 拓地 |
| 嵌套循环配对 | 外层友军 × 内层敌军 | 拓地 |
| break 提前终止 | 找到对手即跳出内层 | 拓地 |
| 精确匹配再行动 | 确认对映体后才 move | 拓地 |
懂得倒着遍历、能把字符串翻转过来、会在茫茫单位中精确配对的英雄,才是驾驭"反转之力"的真正智者。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🔄🏹
扣哒世界 编程知识 · 计算机科学 6(安息云峰):第二单元
扣哒世界教研团队