去做题

第二单元:反转操作 · 倒序遍历与反向匹配

Unit 2: Reversal — Backward Iteration & Reverse Matching
计算机科学 6 · 安息云峰 Cloudrip Mountain · 雪人食人族 → 拓地

2.1 反转操作概述

Overview of Reversal Operations

上一单元你学会了指挥一支队伍、挑选最优目标。这一单元我们要研究一个看似简单、却处处有坑的主题——反转(reversal)。有时候我们手上的数据顺序正好和需要的相反:巫师只能把敌人从最强到最弱排序,可我们必须从最弱到最强逐个击破;术士给每个弓箭手克隆了一个"名字倒着念"的邪恶分身,我们得靠把字符串反转来找出谁是谁的对头。学会反转,就是学会"倒着看世界"。

本单元主线:用 for i in range(len-1, -1, -1) 从后往前倒序遍历列表;用"从末尾逐字符拼接"的算法反转一个字符串;判断"一个字符串反转后是否等于另一个字符串"来做反向匹配;用嵌套循环遍历所有"友军×敌军"的配对组合;并用 break 在找到答案后提前终止循环,避免无谓的计算。

2.1.1 为什么需要反转(Why We Reverse)

2.1.1.1 数据顺序不总合你意 Data doesn't always come in the order you want

编程里经常遇到这种局面:你能拿到的数据,顺序和你需要的正好相反。在"雪人食人族"关卡中,巫师的排序法术只能生成由强到弱的雪人列表,但英雄吞噬生命力的机制要求由弱到强逐个击败。与其请巫师重排(他做不到),不如我们自己倒着读这个列表——这就是倒序遍历的用武之地。

🔄 反转的两种含义
本单元的"反转"有两层意思:一是倒序遍历——列表本身不动,只是我们把访问顺序调转(从最后一个元素往第一个走);二是反转生成——真的造出一个内容颠倒的新字符串(如 "archer" → "rehcra")。前者关心"怎么读",后者关心"造出什么"。

2.1.2 索引与顺序(Indices and Order)

2.1.2.1 最后一个元素的索引是 len - 1 The last element's index is len - 1

要倒着读列表,得先知道"最后一个元素在哪儿"。Python 的索引从 0 开始,所以一个长度为 n 的列表,第一个元素索引是 0,最后一个元素索引是 n - 1(也就是 len(array) - 1)。倒序遍历就是从这个最大索引出发,一步步减到 0。

长度为 4 的列表:索引 0 到 3 "a" "b" "c" "d" 0 1 2 3 = len-1
别用 len(array) 当最后索引:array[len(array)] 会越界报错(IndexError),因为长度为 4 的列表最大合法索引是 3,而不是 4。倒序起点永远是 len(array) - 1,牢记这个"减一"。

2.2 倒序遍历列表

Iterating a List in Reverse

2.2.1 range 的三个参数(The Three Arguments of range)

2.2.1.1 起点、终点、步长 Start, stop, and step

正序遍历我们常写 for i in range(len(array)),它默认从 0 开始、步长为 1、走到 len-1。要倒序,就得用到 range 的完整三参数形式:range(起点, 终点, 步长)。倒序遍历的写法是这个单元最核心的一行代码:

range( len(array)-1 , -1 , -1 ) 起点 = 最后索引 终点 = -1(不含) 步长 = -1(每次减一) 于是 i 依次取 len-1, len-2, ..., 1, 0
# 倒序遍历:从最后一个元素走到第一个
array = [1, 2, 3]
for i in range(len(array) - 1, -1, -1):
    hero.say(array[i])   # 依次说出 3, 2, 1

2.2.2 三个值分别怎么定(How to Choose Each Value)

2.2.2.1 终点为什么是 -1 而不是 0 Why the stop value is -1, not 0

这是倒序遍历最容易搞错的地方。range 的终点值是"取不到"的(左闭右开)。我们希望 i 一直减到 0 并且包含 0(第 0 个元素也要访问),所以终点必须再往下一格写成 -1。如果写成 range(len-1, 0, -1),循环会在 i = 1 时停下,漏掉第 0 个元素。

参数取值含义
起点len(array) - 1从最后一个元素的索引开始
终点-1减到 0 之后再走一步才停,保证包含索引 0
步长-1每次索引减 1,实现"往回走"
口诀记忆:倒序三参数 = "长度减一、负一、负一"。第一个"减一"是起点(最后索引),后面两个"负一"分别是终点和步长。念顺了就不会漏第 0 个元素。

2.2.3 用 while 写倒序(Reverse with while)

2.2.3.1 手动维护索引 Maintaining the index by hand

倒序遍历也能用 while 循环实现,本质完全一样——手动设一个索引,每轮减 1,直到它小于 0。理解这个写法能帮你看清 range 背后到底发生了什么。

for 写法
for i in range(len(array)-1, -1, -1):
    hero.say(array[i])
while 写法
index = len(array) - 1
while index >= 0:
    hero.say(array[index])
    index -= 1
两种写法等价:while index >= 0 和 while index > -1 意思完全相同。用 while 时千万别忘了循环体末尾的 index -= 1,否则索引永远不变,会变成死循环。

2.3 反转字符串

Reversing a String

2.3.1 字符串即字符序列(A String is a Sequence of Characters)

2.3.1.1 把字符串当数组看 Treat a string like an array

反转字符串的秘诀,是意识到字符串和数组本质上是一回事——都是一串按索引排列的元素。数组的元素是任意值,字符串的元素则是单个字符。所以 "archer"[0] 是 "a","archer"[5] 是 "r",len("archer") 是 6。倒序遍历列表的技巧,可以原样搬到字符串上。

字符串 "archer" 的每个字符都有索引 a r c h e r 0 1 2 3 4 5

2.3.2 从末尾拼接算法(Build by Appending from the End)

2.3.2.1 反转字符串的五步法 The five-step string-reversal algorithm

关卡"拓地"给出了标准的字符串反转算法。思路很朴素:准备一个空字符串,然后从原字符串的最后一个字符开始,一个个把它们拼接到新字符串的末尾。原来最后的字符第一个被拼进去,就自然排到了新串的最前面——反转就完成了。

📝 五步算法(来自关卡提示)
1. 创建一个空字符串来存储反转的结果;
2. 从原字符串的最后一个索引开始;
3. 把当前这个字符加到结果字符串后面;
4. 把索引减 1;
5. 重复第 2–4 步,直到到达第一个索引——现在你就得到了一个颠倒的字符串!
# 反转一个字符串:从末尾往前逐字符拼接
def reverseString(text):
    result = ""                    # 1. 空字符串
    for i in range(len(text)-1, -1, -1):   # 2. 从末尾开始倒序
        result += text[i]          # 3. 把字符拼到后面
    return result
"cat" 反转的逐步拼接过程 i=2 result = "t" ← 取 "cat"[2]="t" i=1 result = "ta" ← 取 "cat"[1]="a" i=0 result = "tac" ✓ ← 取 "cat"[0]="c",完成!
+= 拼接的含义:result += text[i] 等价于 result = result + text[i],意思是"把新字符接到已有结果的末尾"。因为我们是倒着取字符、顺着往后接,所以整体效果就是反转。

2.3.3 判断反转后是否相等(Testing if Reversed Equals Another)

2.3.3.1 反转再比较 Reverse, then compare

"拓地"关卡真正要判断的,是两个单位的名字是否互为反向——比如 "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,那样是多余的——比较表达式本身就是一个布尔值。

2.4 嵌套循环配对

Pairing with Nested Loops

2.4.1 两层 for 遍历所有单位对(Two Loops for All Pairs)

2.4.1.1 外层友军,内层敌军 Friends outside, enemies inside

要给每个弓箭手找到它的反向克隆,我们不知道谁配谁,只能把每个友军和每个敌军都比一遍。这需要嵌套循环:外层循环遍历每个友军,内层循环遍历每个敌军,在内层里检查"这一对是不是互为反转"。如果是,就命令这个友军移向对应的敌人。

每个友军都要和每个敌军比一遍 友军 tim 友军 bob 敌军 mit 敌军 bob tim↔mit ✓ 匹配 2 个友军 × 2 个敌军 = 4 次比较
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              # 找到对手就不用再比了

2.4.2 复杂度直觉(A Feel for Complexity)

2.4.2.1 循环次数是乘法关系 The number of iterations multiplies

嵌套循环的一个重要直觉:总的比较次数是两层循环长度的乘积。如果有 5 个友军、5 个敌军,内层就会执行 5×5 = 25 次。友军和敌军越多,比较次数增长得越快。这也是为什么下面要讲的 break 很有价值——它能在找到答案后省下剩余的无用比较。

为什么先反转再嵌套:注意 areAntipodes 内部本身还有一个反转字符串的循环。所以这里其实是"三层"计算:外层友军、内层敌军、最内层反转字符。理解这一点,你就能明白代码到底做了多少工作。

2.5 break 提前终止

Ending Loops Early with break

2.5.1 找到即停(Stop as Soon as You Find It)

2.5.1.1 break 立即跳出当前循环 break exits the current loop immediately

每个弓箭手只有一个反向对手。一旦在内层循环里找到了它、并下达了移动命令,就没必要再把剩下的敌人比完了。break 语句会立即跳出它所在的那一层循环,让外层循环去处理下一个友军。这既符合逻辑,又省下计算。

for enemy in enemies:
    if areAntipodes(friend, enemy):
        hero.command(friend, "move", enemy.pos)
        break   # 已找到对手,跳出内层循环
🚪 break 就像"找到就走人"
想象你在一排储物柜里找自己的柜子。一旦找到写着你名字的那个,你就直接停下、拿东西离开,绝不会继续把剩下的柜子挨个打开检查。break 做的正是这件事——目标达成,立刻退出循环。
break 与 continue 的区别

break:彻底跳出整个循环,后面的元素一个都不再看。

 

continue:只跳过当前这一轮,循环仍会继续处理后面的元素。

break 只跳一层:在嵌套循环里,内层的 break 只会跳出内层,外层循环照常继续。所以上面的代码里,一个友军找到对手后 break,外层会立刻接着处理下一个友军——这正是我们想要的。

2.6 实战关卡精讲

Level Strategy & Analysis

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

关卡 · 雪人食人族

❄️ 雪人食人族 —— 倒序击败雪人

扣哒世界 雪人食人族关卡 - koudashijie.com
图2:雪人食人族——巫师把雪人从强到弱排好序,英雄需倒序遍历该列表,从最弱的雪人开始逐个击败并吞噬

策略:由弱到强,倒着打

本关的核心机制是:英雄击败雪人时会吞噬它的生命力,所以必须从最弱的开始打,越打越强,才能应付后面更强的雪人。麻烦在于——巫师的排序法术只能把雪人排成从最强到最弱的列表。既然列表方向反了,我们就倒序遍历这个列表:从最后一个(最弱的)打到第一个(最强的)。

思路拆解

第一步,拿到排好序的列表:找到巫师朋友,调用它的 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 内层:攻击一次未必能杀死一个雪人。用 while yetis[i].health > 0 内层循环,保证对当前这个雪人持续攻击、直到它彻底倒下,再由外层 for 推进到下一个更强的雪人。这是"外层选目标、内层打到死"的经典结构。
关键在起点和步长:本关成败全系于那一行 range(len(yetis)-1, -1, -1)。起点写成 len(yetis)-1 才是最弱雪人;步长 -1 才能往回走。任何一处写错,就会先打到强雪人而被反杀。
关卡 · 拓地

🏹 拓地 —— 反向名字匹配对手

扣哒世界 拓地关卡 - koudashijie.com
图3:拓地——术士克隆出名字反向的邪恶弓箭手,需为每个友军弓箭手找到名字互为反转的克隆并令其接触,最后击败术士

策略:找到反灵魂伴侣去湮灭它

术士用"克隆"法术造出了我方弓箭手的邪恶镜像——每个克隆的名字都是某个友军名字的反向拼写(如友军 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)
别碰错克隆:本关最大的陷阱是"接触错误的克隆会激活它"。所以绝不能让友军盲目移向最近的敌人,必须先用 areAntipodes 确认是它真正的对手,再下 move 命令。这正是精确匹配比"就近处理"更重要的场景。
先湮灭克隆,再打术士:提示里说"术士看似平静,但能感受到任何侵略"。含义是:应当先安静地把克隆一个个匹配湮灭,等场面清干净了再集火术士,避免过早惊动它、陷入多线作战。

2.7 常见错误

Common Mistakes

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

2.7.1.1 反转操作的典型问题 Typical issues with reversal

扣哒世界 反转操作常见错误速查 - koudashijie.com
图4:起点没减一、终点写成 0、步长忘记负号、结果字符串没初始化,是本单元最常见的失误
易错点清单:
  • 起点忘记减一——倒序起点必须是 len(array) - 1,直接写 len(array) 会索引越界报错。
  • 终点写成 0 而非 -1——range(len-1, 0, -1) 会漏掉第 0 个元素(最强雪人或名字首字符),终点必须是 -1。
  • 步长忘记负号——range(len-1, -1, 1) 步长为正、起点又大于终点,循环一次都不执行。倒序步长必须是 -1。
  • 结果字符串没初始化——反转前要先 result = "",否则拼接时会因变量未定义而报错。
  • 用 + 号连接字符时用错变量——务必是 result += text[i],取的是当前索引的字符,不要写死成某个固定下标。
  • 嵌套循环里 break 位置错——break 要放在"找到对手并下命令之后",且只跳内层;放错会导致提前中断或永不停止。
  • while 倒序忘记减索引——用 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(安息云峰):第二单元

扣哒世界教研团队