欢迎来到计算机科学 5 的第十六单元!之前你已经会创建数组、遍历数组、读取元素了。但数组不只是"只读的清单"——它还能被不断修改,用来记录一群对象随时间变化的状态。本单元你将学会一项在游戏开发、库存管理、任务调度里无处不在的核心技巧:状态数组(state array)。你要帮助盲人牧民 Merek,用一个数组记录哪些驯鹿醒着、哪些围栏空着,并把驯鹿一只只安排进圈里。
i 只驯鹿的状态放在 deerStatus[i]。你将学会用 deerStatus[i] = "awake" 修改元素、用嵌套循环做位置匹配并更新状态、用 None 表示"空位/已删除"、以及查找数组里第一个空位来分配新对象。这套技巧让你能"追踪一群东西的当前状态并动态调整"。
状态数组的核心思想是"平行":你有一组对象(比如 5 只驯鹿),再单独建一个同样长度的数组,专门记录它们各自的状态。关卡"驯鹿苏醒"一开始就给了这样一个数组,5 只驯鹿全都初始化为 'asleep'(睡着)。随着程序运行,你把醒来的驯鹿在数组里对应位置改成 'awake'。
# 这个数组记录每只驯鹿的状态(初始全部睡着) deerStatus = ['asleep', 'asleep', 'asleep', 'asleep', 'asleep'] # 这个数组是驯鹿本身 friends = hero.findFriends()
friends),又有一张同样顺序的"出勤表"(deerStatus)。名单第 2 位的同学,出勤表第 2 格就记录他"到"还是"缺席"。两张表顺序一一对应,靠同一个索引把"是谁"和"状态如何"连接起来。
平行数组之所以能配合,是因为它们共用同一套索引。friends[2] 是第 3 只驯鹿这个对象,deerStatus[2] 就是它的状态。只要保证两个数组顺序不乱,i 就同时是"第几只驯鹿"和"它的状态在哪一格"。这就是为什么遍历时我们常用 range(len(...)) 拿到索引——索引本身携带了"身份"信息。
数组不是刻在石头上的——你可以随时把某个位置的元素换成新值。语法很简单:把 array[i] 放在等号左边,右边写新值。关卡说明里给了最直观的演示:读取用 a[1],赋值也用 a[1],只是放在等号左边。
a = [None, 'no', 'maybe'] hero.say(a[1]) # 说 "no" a[1] = 'yes' # ← 把索引 1 的元素改成 'yes' hero.say(a[1]) # 说 "yes"
你不一定非得从头到尾遍历。只要元素存在,就能以任意顺序读取或修改任意位置——这叫"随机访问"。但关卡也反复提醒:索引必须在有效范围内。一个长度为 3 的数组,合法索引是 0、1、2;访问 a[3] 就会越界报错(这正好呼应上一单元的边界防护)。
a = [None, 'one', 'two'] hero.say(a[1]) # 说 "one" hero.say(a[2]) # 说 "two" hero.say(a[0]) # 什么都不说(元素是 None) hero.say(a[3]) # 错误!数组从 0 开始,到 2 结束
a[0] 的值是 None——一个专门表示"空、无、还没有值"的特殊对象。让英雄去说 None,他什么都不会说。这个特性在本单元后面表示"空位"时会大显身手。
很多时候,你需要把两组东西"配对"——比如判断每只驯鹿站在哪个围栏上。做法是嵌套循环:外层遍历每只驯鹿,内层遍历每个围栏位置,逐一比较坐标。当某只驯鹿的 x 和 y 都和某个围栏相等时,就"匹配成功",把该围栏对应位置的状态更新为这只驯鹿。这正是"驯鹿观察者"关卡的核心算法。
# 外层:遍历每只驯鹿 for deerIndex in range(len(friends)): reindeer = friends[deerIndex] # 内层:遍历每个围栏位置 for penIndex in range(len(penPositions)): penPos = penPositions[penIndex] if penPos.x == reindeer.pos.x and penPos.y == reindeer.pos.y: penOccupants[penIndex] = reindeer.id # 匹配成功,记录 break # 找到就停
一只驯鹿只可能站在一个围栏上。所以一旦内层循环找到匹配的围栏、更新完状态,就该用 break 立刻跳出内层循环——继续比对剩下的围栏毫无意义,还浪费时间。break 只跳出它所在的那一层循环(这里是内层围栏循环),外层驯鹿循环会继续处理下一只驯鹿。
break 只结束最内层循环,不会连外层一起结束。所以驯鹿匹配到围栏后 break,程序会自然回到外层去看下一只驯鹿,非常适合"每个对象找到归宿就收手"的场景。
在"驯鹿招标"关卡里,你需要把已经归位的驯鹿从待分配名单中"移除"。但直接删掉数组元素会让后面所有元素的索引都往前挪、打乱平行关系。更安全的做法是:把那个位置设为 None——数组长度不变、索引不乱,只是这一格被标记为"空、已处理、无需再管"。关卡默认代码里 penOccupants 一开始就全是 None,表示所有围栏都空着。
# 围栏占用情况,初始全空(用 None 表示空位) penOccupants = [None, None, None, None, None] # 驯鹿归位后,把它从待分配名单里"抹掉" friends[deerIndex] = None # ← 长度不变,只是这格变空
friends[i] = None 只是把该位置的内容清空,数组仍然有 5 个格子。后面遍历时你会遇到这些 None,必须主动跳过它们(比如用 if not reindeer: continue),否则会把"空位"当成驯鹿去处理而出错。
"分配"类问题有一个经典算法:找第一个空位。做法是从索引 0 开始遍历目标数组,用 if not penOccupants[occIndex]: 判断这一格是不是空的(None 在布尔判断里是"假",所以 not None 为真)。找到第一个空位后,就把对象放进去,并立刻 break 退出——这样才能保证每个对象只占一个位置、不会重复分配。
# 寻找第一个没有驯鹿的圈 for occIndex in range(len(penOccupants)): if not penOccupants[occIndex]: # 这一格是空的(None / 假) penOccupants[occIndex] = reindeer # 放进去 hero.command(reindeer, 'move', penPositions[occIndex]) break # 找到就停,避免重复分配
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关是状态数组的入门关。盲人牧民 Merek 看不见,需要你告诉他每只驯鹿是醒是睡。判断依据很简单:不在圈里(y 坐标 > 30)的驯鹿一定是醒着的。策略分两步——先遍历所有驯鹿,把醒着的在 deerStatus 里标记为 'awake';再遍历 deerStatus,把每只驯鹿的索引和状态"说"给 Merek。他听到"asleep"就会命令那只驯鹿醒来。
算法有两个循环。第一个循环更新状态:用 range(len(friends)) 遍历拿到 deerIndex,取出 reindeer = friends[deerIndex],若 reindeer.pos.y > 30,就执行核心一步 deerStatus[deerIndex] = 'awake'——注意这里用同一个索引 deerIndex 同时定位驯鹿和它的状态格,这就是平行数组的精髓。第二个循环负责报告:遍历 deerStatus,用字符串拼接说出"Reindeer + 索引 + is + 状态"。
# 关键片段一:按位置更新状态数组 for deerIndex in range(len(friends)): reindeer = friends[deerIndex] if reindeer.pos.y > 30: # 不在圈里 = 醒着 deerStatus[deerIndex] = 'awake' # 同索引定位状态 # 关键片段二:遍历状态数组,报告给 Merek for statusIndex in range(len(deerStatus)): hero.say('Reindeer ' + statusIndex + ' is ' + deerStatus[statusIndex])
friends 和 deerStatus),必须靠共享的索引 deerIndex 把二者对齐。如果只用 for reindeer in friends,就拿不到能定位状态格的索引了。
deerStatus 一开始全是 'asleep',所以你只需把"醒着的"改成 'awake',没被改动的自然保持"睡着"。这种"先假设一个默认状态,再修正例外"的写法非常常见。
本关引入嵌套循环 + 位置匹配。Merek 想把驯鹿放进围栏过夜,但不知道哪些围栏已经有驯鹿了。你有围栏位置数组 penPositions、占用状态数组 penOccupants(初始全是 'empty')和驯鹿数组 friends。策略是:通过比对坐标,找出每只已站在围栏上的驯鹿,把它的 id 记入对应围栏的 penOccupants;最后报告每个围栏的占用情况,Merek 听到 'empty' 就会派驯鹿过去。
核心是双层循环的位置匹配:外层遍历每只驯鹿,内层遍历每个围栏位置。当 penPos.x == reindeer.pos.x and penPos.y == reindeer.pos.y 时,说明这只驯鹿正站在这个围栏上,于是执行 penOccupants[penIndex] = reindeer.id 记录,并 break 跳出内层——一只驯鹿只占一个围栏,找到即停。注意这里存的是 reindeer.id(驯鹿的标识),而不是整只驯鹿对象,因为 Merek 只需要知道"是哪一只"。
# 关键片段:嵌套循环匹配坐标,记录占用 for deerIndex in range(len(friends)): reindeer = friends[deerIndex] for penIndex in range(len(penPositions)): penPos = penPositions[penIndex] if penPos.x == reindeer.pos.x and penPos.y == reindeer.pos.y: penOccupants[penIndex] = reindeer.id # 记录 id break # 跳出内层
x 和 y(用 and 连接)。只比 x 相等是不够的——不同围栏可能横坐标相同、纵坐标不同。只有两个坐标都吻合,才能确定驯鹿真的在这个围栏上。
本关是本单元的压轴关,综合运用了修改元素、None 删除、查找空位三大技巧。任务是让每只驯鹿都睡进一个圈里。策略分两轮:第一轮找出已经站在圈里的驯鹿,把它们记入 penOccupants,同时把它们从 friends 里设为 None(表示"已处理,无需再分配");第二轮遍历剩下的驯鹿,为每只找一个第一个空圈,命令它移动过去。
和"驯鹿观察者"类似,用嵌套循环做位置匹配。区别在于匹配成功后做两件事:penOccupants[penIndex] = reindeer 把驯鹿存进占用数组,并且 friends[deerIndex] = None 把它从待分配名单里抹掉。这样第二轮就不会重复处理已归位的驯鹿了。
# 第一轮关键片段:匹配成功后登记 + 移除 if penPos.x == reindeer.pos.x and penPos.y == reindeer.pos.y: penOccupants[penIndex] = reindeer # 登记进占用数组 friends[deerIndex] = None # 从名单里"删除" break
再次遍历 friends。由于已归位的驯鹿被设成了 None,遇到它们要用 if not reindeer: continue 跳过。对于真正剩下的驯鹿,用"查找第一个空位"算法遍历 penOccupants,if not penOccupants[occIndex]: 找到第一个空圈,就把驯鹿放进去、命令它 move 过去,然后 break 提前退出,防止一只驯鹿被分到多个圈。
# 第二轮关键片段:跳过 None,为剩下的驯鹿找空圈 for deerIndex in range(len(friends)): reindeer = friends[deerIndex] if not reindeer: # 是 None(已归位),跳过 continue for occIndex in range(len(penOccupants)): if not penOccupants[occIndex]: # 第一个空圈 penOccupants[occIndex] = reindeer hero.command(reindeer, 'move', penPositions[occIndex]) break # 分配一个就停
continue 是"跳过本次循环的剩余部分,直接进入下一轮"——用来略过 None 空位;break 是"彻底结束当前循环"——用来在找到空圈分配后停手。用错会导致驯鹿分配混乱:该跳过的没跳过、该停的没停。
hero.command(reindeer, 'move', ...) 亲自指挥驯鹿移动到空圈——命令需要驯鹿对象本身,所以 penOccupants 里存的是 reindeer 而不是它的 id。
deerStatus[deerIndex] 而不是别的变量,否则改错了驯鹿的状态。None 后再遍历,一定要用 if not x: continue 跳过空位,否则会把空位当对象处理而报错。break,内层循环会继续跑,可能重复分配或做无用功。continue,分配完成该用 break,用反了逻辑就乱。x 和 y 都用 and 判断相等,才能确认真的在同一位置。deerStatus[i] 里的 i 是位置,元素的值是状态,别把状态字符串拿去当索引用。friends[i] = None 不改变长度,只是清空该格,len(friends) 依然不变。id;还要指挥移动就存整只驯鹿对象,按用途选择存什么。恭喜你掌握了"状态数组"这项强大的追踪技能!这一单元你理解了平行数组的核心思想——用同一个索引把"对象"和"它的状态"连接起来。你学会了用 array[i] = 新值 修改任意位置的元素、用嵌套循环做位置匹配并更新状态、用 None 表示"空位或已删除"、以及"查找第一个空位"这一经典分配算法;还弄清了 break(结束循环)和 continue(跳过本轮)的区别。在"驯鹿苏醒、驯鹿观察者、驯鹿招标"三关里,你从单层更新状态,一步步走到"移除元素 + 重新分配"的综合应用。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| 平行状态数组 | 同索引连接对象与状态 | 三关 |
| 按索引赋值 | array[i] = 新值 | 驯鹿苏醒 |
| 随机访问 | 任意顺序读写存在的元素 | 三关 |
| 嵌套循环匹配 | 外层对象、内层位置比对 | 观察者/招标 |
| break 提前退出 | 匹配到就跳出内层 | 观察者/招标 |
| None 表示删除 | 清空该格,长度不变 | 驯鹿招标 |
| 跳过空位 continue | if not x: continue | 驯鹿招标 |
| 查找第一个空位 | 从头扫描,遇空即分配 | 驯鹿招标 |
懂得用一个数组追踪万物状态、把对象井井有条地登记与分配的英雄,才是云峰之上真正周全的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🦌📋
扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十六单元
扣哒世界教研团队