欢迎来到计算机科学 5 的第十五单元!在之前的单元里,你已经学会了怎样用嵌套循环遍历二维数组、用 grid[i][j] 读取每一个格子。可是当地图不再"方方正正"、当坐标来自随机或外部来源时,一个粗心的索引就会让程序崩溃。本单元你将学会一件专业程序员每天都在做的事——边界检查(bounds checking):在访问数组元素之前,先确认这个位置真的存在。
grid[row][col] 前,先用 row < len(grid) 确认行存在,再用 col < len(grid[row]) 确认列存在。只有两条都成立,才安全地读取那个格子。这套"先检查、后访问"的习惯能彻底避免 IndexError 索引越界异常,尤其是在每行长度都不一样的不规则数组上。
数组的索引是从 0 开始编号的。一个有 3 个元素的数组,合法索引只有 0、1、2——最大索引永远是长度减一。如果你去访问 grid[3],就像伸手去够一个根本不存在的格子:程序无法凭空造出数据,只能报错停下。二维数组更容易出错,因为你要同时保证"行"和"列"两个索引都合法。
0 到 长度-1 之间,超出这个范围,Python 就会"查无此址"地报错。
当访问的索引超出数组范围时,Python 会抛出 IndexError(列表越界)异常。关卡"雪花莲"的说明就直接演示了两种典型的越界:行对但列错,以及行本身就不存在。这两种情况都会让程序中断——在游戏里,就意味着大炮朝着不存在的坐标开火、任务失败。
# 假设 forestMap 的第 0 行只有 3 个元素(索引 0、1、2) # 正确的行,但错误的列: forestMap[0][4] # ← IndexError:第 0 行没有第 4 列 # 错误的行,任意列: forestMap[5][1] # ← IndexError:forestMap[5] 根本不存在
正如"大炮陆战队"关卡所说:"二维数组是每个元素都是数组的数组。因此,我们得到了类似电子表格的东西。"外层数组的每个元素本身又是一个数组——外层的索引是"行",内层的索引是"列"。要取出某个格子,就写两对方括号:先选行,再选列。
# 一个 2 行 3 列的二维数组 grid = [ [0, 1, 2], # ← 第 0 行 [3, 4, 5] # ← 第 1 行 ] grid[0][0] # 是 0(第 0 行、第 0 列) grid[1][2] # 是 5(第 1 行、第 2 列) grid[1][0] # 是 3(第 1 行、第 0 列)
要做边界检查,先得知道数组"有多大"。函数 len() 返回一个数组里元素的个数。对二维数组来说,len(grid) 是行数;而 len(grid[row]) 是第 row 行的列数。这正是边界检查的两把"尺子"。
grid = [[0, 1, 2], [3, 4, 5]] len(grid) # 2 —— 一共有 2 行 len(grid[0]) # 3 —— 第 0 行有 3 列 len(grid[1]) # 3 —— 第 1 行有 3 列
len(grid) 是 2,那么合法的行索引就是 0 和 1——最大索引 = 长度 − 1。所以判断"索引合法"的条件是 索引 < 长度,而不是 索引 <= 长度。这个"小于"非常关键,多一个等号就会越界。
安全访问的第一步:确认行索引 row 落在合法范围内,也就是 row < len(grid)。只有这一行真的存在,我们才有资格去看它有几列。如果连行都不存在,谈论"列"就毫无意义——就像先要确认门牌号所在的那条街存在,才能谈门牌号。
row = 1 if row < len(forestMap): # 到这里,说明 row 是一个合法的行索引 # ...接着才检查列... pass
确认行存在之后,第二步是检查列索引:col < len(forestMap[row])。注意这里用的是 len(forestMap[row])——是那一行自己的长度,而不是整个数组的长度。这一点在不规则数组里尤其重要,因为每行的列数可能都不一样。关卡"雪花莲"给出的正是这个"两层嵌套 if"的标准写法:
row = 1 col = 3 if row < len(forestMap): # 如果 row 是正确的索引 if col < len(forestMap[row]): # 现在我们才能安全地取到单元格 hero.say(forestMap[row][col])
两层嵌套的 if 逻辑清晰,但也可以用逻辑运算符 and 写得更简洁。关键在于 and 的短路特性:它先算左边,只有左边为真才会去算右边。这恰好符合我们的需要——先确认 row 合法,再去算 len(forestMap[row])。如果行不存在,右边根本不会执行,也就不会触发越界。
# 第二部分仅在第一部分为真时才检查(短路求值) if row < len(forestMap) and col < len(forestMap[row]): hero.say(forestMap[row][col])
row < len(forestMap),再写 col < len(forestMap[row])。如果把两者对调,那么当 row 越界时,Python 会先去算 forestMap[row] 来取长度——这本身就是一次越界访问,程序当场崩溃,短路保护也就失效了。
if row < len(grid): if col < len(grid[row]): use(grid[row][col])
if row < len(grid) and \ col < len(grid[row]): use(grid[row][col])
大多数地图都是整齐的矩形,每行列数相同。但二维数组并不要求这样——正如"雪花莲"关卡强调的:"数组各行的长度不同。所以我们在访问前需要检查坐标的正确性。"这种每行长度不一的数组叫做不规则数组(jagged array)。它正是边界检查的"用武之地":因为你无法用一个统一的列数去套所有行。
# 一个不规则的森林地图:每行列数都不一样 forestMap = [ [1, 1, 0], # 第 0 行:3 列(索引 0~2) [0, 1], # 第 1 行:2 列(索引 0~1) [1, 1, 0, 0, 1] # 第 2 行:5 列(索引 0~4) ]
len(grid[row]):在上图里,第 1 行只有 2 列。如果你想访问 grid[1][2],虽然行 1 存在,但该行没有索引 2 的列——用整体列数去判断就会出错,只有用该行自己的长度 len(grid[1]) 才能得到正确的 2。这就是"逐行测量"的意义。
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关是本单元的入门关,先复习"规则二维数组"的遍历。村里混进了梦游的雪人,我们要按猎人 Senick 的fenceMap(一个由 0 和 1 组成的网格)建立栅栏系统:1 是要建栅栏的地方,0 是空格。核心约束是不能用战斗手段(会惊醒雪人),只能悄悄筑墙,让熟睡的农民通过、把雪人拦在外面。建完栅栏后英雄要立刻回村,因为雪人能察觉到英雄。
算法是标准的嵌套循环遍历二维数组:外层 for i in range(len(fenceMap)) 逐行,内层 for j in range(len(fenceMap[i])) 逐列。对每个格子,如果 fenceMap[i][j] 为 1(真),就用关卡预先给好的 convertCoor(i, j) 把"行列坐标"转成游戏世界的 (x, y) 坐标,移动过去并 buildXY("fence", ...) 建栏。注意内层用 len(fenceMap[i]) 逐行取列数——这是养成安全习惯的开始,即使本关网格是规则的。
# 关键片段:嵌套循环遍历 fenceMap,遇 1 就建栏 for i in range(len(fenceMap)): for j in range(len(fenceMap[i])): if fenceMap[i][j]: # 值为 1 即为真 pos = convertCoor(i, j) hero.moveXY(pos['x'], pos['y']) hero.buildXY("fence", pos['x'], pos['y'])
convertCoor),所以用 range(len(...)) 遍历索引最合适——这样 i、j 就是现成的行列坐标。
hero.moveXY(29, 17) 回到安全的村子里。别筑完栏还傻站在雪人旁边。
本关用大炮把士兵和"反陷阱弹"发射到山上的村庄广场。着陆区地图 landingMap 是一个字符串二维数组,每个格子是 "peasant"(农民)、"trap"(陷阱)或 "clear"(空地)。你向大炮"说出"行、列和那里的内容,大炮听到 "trap" 或 "clear" 就会开火。策略的核心是精确:广场上有农民,必须只报出示例代码给定的正确坐标,绝不能报错坐标——否则炮弹会打偏或误伤农民。
本关不需要循环,而是练习用 array[i][j] 精确读取指定格子并说出来。关卡给出了一串要处理的坐标点(如第 2 行第 1 列、第 1 行第 0 列……),你只需依次用 landingMap[row][col] 取值、用 hero.say() 报出坐标与内容即可。关键教训是"越界的后果":关卡演示了如果写成 landingMap[9001][19] 这种超范围的索引,就会触发引用/索引错误、大炮 BOOM 打偏。所以务必照给定坐标读取,行列不要写反、不要超范围。
# 关键片段:读取指定坐标的值并报出 cell = landingMap[0][0] # 第 0 行第 0 列 hero.say("行0列0 " + cell) hero.say("行2列1 " + landingMap[2][1]) hero.say("行1列0 " + landingMap[1][0]) hero.say("行0列2 " + landingMap[0][2]) hero.say("行1列3 " + landingMap[1][3])
landingMap[2][1] 是"第 2 行第 1 列",而 landingMap[1][2] 是"第 1 行第 2 列"——两者完全不同。第一个方括号永远是行,第二个永远是列。写反了大炮就打错地方。
landingMap 每行列数相同(规则矩形),所以照给定坐标读取就很安全。但关卡也提醒你:"里面的数组可以有不同的长度"——这正是下一关"雪花莲"要面对的挑战,也是本单元边界检查的真正战场。
本关是本单元的压轴关,把"边界检查"用到极致。森林地图 forestMap 是不规则二维数组:1 是安全的空地,0 是陷阱,而且每行长度都不同。任务是清除陷阱,但弹药珍贵、只够清理陷阱的数量,还不能乱射引起火灾。关卡给了你一串要检查的坐标点数组 cells(其中有些坐标可能越界!),策略核心是:对每个坐标先做边界检查,确认格子存在、且值为 0(陷阱),才报出坐标开火。
算法分三层判断,缺一不可。第一,遍历 cells,从每个字典里取出 row 和 col。第二,做两级边界检查:先 if row < len(forestMap) 确认行存在,再 if col < len(forestMap[row]) 确认这一行有这一列——因为每行长度不同,必须用 len(forestMap[row]) 逐行测量。第三,只有前两关都通过、且 forestMap[row][col] == 0(是陷阱)时,才 hero.say() 报出"行 X 列 Y 开炮!"。这样既不会越界崩溃,也不会浪费弹药朝空地开火。
# 关键片段:遍历坐标点,三层判断后才开火 for cell in cells: row = cell["row"] col = cell["col"] if row < len(forestMap): # ① 行存在吗? if col < len(forestMap[row]): # ② 这一行有这一列吗? if forestMap[row][col] == 0: # ③ 是陷阱吗? hero.say("行" + row + "列" + col + "开炮!")
and 合成一句:if row < len(forestMap) and col < len(forestMap[row]):,再在里面判断是否为陷阱。效果完全相同,代码更扁平。
cells 里故意放了越界坐标。如果你省掉边界检查,直接写 if forestMap[row][col] == 0,一遇到越界坐标就会抛出 IndexError,程序崩溃、任务失败。这正是本单元要教会你的核心教训:坐标来自外部时,访问前必须检查。
row 再查 col;先访问 forestMap[row] 取长度而行已越界,会当场崩溃,短路保护失效。len(forestMap[row])(该行的列数),而不是 len(forestMap)(行数)或 len(forestMap[0])(只有第 0 行的列数)。< 写成 <=——合法索引是 0 到 长度-1,用 索引 < 长度 判断;写成 <= 会正好越界一格。grid[row][col] 和 grid[col][row] 含义完全不同。cells 数组)时,直接访问极易越界崩溃。cells 时要先 cell["row"]、cell["col"] 取出坐标,别直接把字典当索引用。forestMap[row][col] == 0 判断的是格子里的值(是不是陷阱),而 row、col 是位置,两者别搞混。恭喜你掌握了一项专业程序员每天都在用的防御技能——边界检查!这一单元你理解了索引从 0 开始、最大索引等于长度减一,越过范围就会抛出 IndexError 让程序崩溃。你学会了安全访问二维数组的黄金模式:先用 row < len(grid) 确认行存在,再用 col < len(grid[row]) 确认列存在,两条都成立才读取 grid[row][col];也学会了用 and 的短路特性把两步合成一句。你还认识了每行长度不同的不规则数组,明白了为什么列检查必须用"该行自己的长度"。在"梦游者、大炮陆战队、雪花莲"三关里,你从规则网格的遍历,一步步走到不规则地图上的安全访问。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| 索引从 0 开始 | 最大索引 = 长度 − 1 | 三关 |
| IndexError 异常 | 越界访问会崩溃终止 | 雪花莲 |
| len 测长度 | len(grid) 行数 / len(grid[r]) 列数 | 三关 |
| 检查行 | row < len(grid) | 雪花莲 |
| 检查列 | col < len(grid[row]) | 雪花莲 |
| and 短路合并 | 先真才算右边,顺序不能反 | 雪花莲 |
| 不规则行长度 | 每行列数不同需逐行测量 | 雪花莲 |
| 精确读取 grid[i][j] | 第一括号行、第二括号列 | 大炮陆战队 |
| 嵌套循环遍历 | range(len(...)) 逐行逐列 | 梦游者 |
懂得在伸手之前先看清脚下、在访问之前先确认存在的英雄,才是云峰之上真正稳健的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️❄️🛡️
扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十五单元
扣哒世界教研团队