去做题

第十单元:过滤筛选 · 把数据分门别类

Unit 10: Filtering — Sorting Data into Buckets
计算机科学 5 · 安息云峰 Cloudrip Mountain · 第 16 关:资源谷

10.1 什么是过滤筛选

What Is Filtering

黄金峡谷、银色沼泽、青铜盆地——三座山谷里散落着不同价值的硬币。你手下有三个农民,每人只能进自己那片区域,而每个区域只产出一种价值的硬币。麻烦来了:hero.findItems() 一次性把所有硬币都返回给你,混在同一个数组里。如果直接让农民去捡"最近的硬币",他们会瞄准错误的目标、乱作一团。你需要一件新工具,把这一大堆硬币按价值分拣成三堆,再各自交给对应的农民——这就是本单元的主题:过滤筛选(filtering)。

本单元主线:过滤就是"从一个大数组里,按某个条件挑出符合的元素,放进新数组"。你将学会遍历原数组、用 if 检查每个元素的属性(如 item.value)、用 append() 把合格的元素追加到新数组,再用 if/elif 一次遍历把硬币分成金、银、铜三类,最后把每个分类数组作为参数传给函数去处理。这是数据处理和 AI 里最基础、最常用的操作之一。

10.1.1 从"一堆"到"分类"(From a Pile to Buckets)

10.1.1.1 过滤 = 保留或去除某些元素 Filtering keeps or removes elements

关卡概览定义得很清楚:"过滤是保留或去除某些元素的过程。"想象你面前有一大筐混装的硬币,你拿三个空篮子,一枚一枚检查:金币放金篮、银币放银篮、铜币放铜篮。检查完,原来乱糟糟的一筐,就变成了整整齐齐的三篮。程序里,"筐"是原数组,"篮子"是新数组,"一枚一枚检查"是循环,"是不是金币"是条件判断。

扣哒世界 过滤分类示意 - koudashijie.com
图1:过滤把混装的硬币按价值分拣进三个数组——金币、银币、铜币各归其位
🧺 过滤就像"分拣快递"
原数组:一大车混在一起的包裹(所有硬币)。
遍历:分拣员一个一个拿起包裹检查。
条件:看包裹的目的地标签(item.value)。
append:放进对应城市的货筐(金/银/铜数组)。
分拣完,每个货筐里都是同一目的地的包裹,直接发车即可。

10.1.2 过滤的三要素(The Three Ingredients)

10.1.2.1 遍历、条件、收集 Iterate, test, collect

任何过滤都由三个动作组成,缺一不可:遍历(把原数组的每个元素都看一遍)、条件(用 if 判断这个元素要不要)、收集(合格的用 append 放进新数组)。记住这三步,你就能应对几乎所有筛选任务。

过滤的三个动作 ① 遍历 for item in items ② 条件 if item.value == 3 ③ 收集 goldCoins.append(item)

10.2 遍历 + 条件筛选

Iterating and Testing

10.2.1 遍历原数组(Looping Over the Source)

10.2.1.1 两种遍历写法 Two ways to loop

遍历数组有两种常见写法。一种是 for item in items:,直接把每个元素赋给 item,简洁好读;另一种是 for i in range(len(items)): 用下标访问 items[i],当你需要下标时才用它。关卡默认代码用的是前者。

直接遍历元素(推荐)
for item in items:
    # item 就是当前元素
    ...
下标遍历(需要 i 时)
for i in range(len(items)):
    item = items[i]
    ...

10.2.2 用 if 判断 value(Testing item.value)

10.2.2.1 按属性值筛选 Filter by an attribute's value

每枚硬币都有一个 value 属性表示它的价值:金币 value == 3、银币 value == 2、铜币 value == 1。筛选的核心就是用 == 检查这个属性等于几。关卡概览还展示了另一个例子——按 enemy.type == "thrower" 筛出所有投掷手,思路完全一样,只是把"按 value 筛"换成了"按 type 筛"。

# 按属性筛选:value 或 type 都行
if item.value == 3:      # 金币
    ...
if enemy.type == "thrower":  # 投掷手
    ...
value 的含义要读懂:本关三座山谷各产一种价值的硬币——金币值 3、银币值 2、铜币值 1。之所以能用 value 精确区分,是因为每类硬币的 value 唯一。遇到别的任务,你要先弄清"用哪个属性、等于什么值"才能筛对。

10.3 append 追加

Appending to a New Array

10.3.1 新建空数组(Start with an Empty List)

10.3.1.1 先备好空篮子 Prepare the empty buckets first

过滤前,要先为每个分类准备一个空数组——用 [] 创建。关卡里在 while True 循环内,每一帧都重新建三个空数组 goldCoins = []、silverCoins = []、bronzeCoins = []。这一步很重要:每帧重置,才能装当前这一帧新扫描到的硬币,不会和上一帧的混起来。

# 每帧先备好三个空篮子
goldCoins = []
silverCoins = []
bronzeCoins = []

10.3.2 append 与 push(append vs. push)

10.3.2.1 把元素放进数组末尾 Add an element to the end

数组.append(元素) 把一个元素追加到数组末尾,数组随之变长。Python 用 append,而关卡概览里出现的 throwers.push(enemy) 是 JavaScript 的写法,作用相同——都是"压入一个元素"。写 Python 时统一用 append。

Python:append
goldCoins.append(item)
# 追加到末尾
JavaScript:push(对照)
throwers.push(enemy);
// 同样是压入末尾
append 把新元素接到数组末尾 金 金 + append(新金币) 新元素追加到这里

10.4 多路分类

Multi-Way Classification

10.4.1 if/elif 分流(Branching with if/elif)

10.4.1.1 三种价值,三条分支 Three values, three branches

本关不是"筛一类",而是"分三类"。用 if / elif / elif 把三种价值分别导向三个数组:value == 3 进金币数组,value == 2 进银币数组,value == 1 进铜币数组。用 elif 而不是三个独立的 if,是因为一枚硬币只可能属于一类,匹配到就无需再检查其余分支,更高效。

10.4.2 一次遍历分三类(One Pass, Three Buckets)

10.4.2.1 遍历一遍就能全部分好 Sort everything in a single loop

关键在于:不需要遍历三遍。只在一个 for 循环里,对每枚硬币用 if/elif 判断一次,就把它投进正确的篮子。默认代码已经给好金币那一支(if item.value == 3: goldCoins.append(item)),留下银币、铜币两支让你补全。

# 一次遍历,if/elif 把硬币分进三个数组
for item in items:
    if item.value == 3:
        goldCoins.append(item)
    elif item.value == 2:
        silverCoins.append(item)
    elif item.value == 1:
        bronzeCoins.append(item)
扫描到的硬币value命中分支进入数组
金币3ifgoldCoins
铜币1elif(1)bronzeCoins
银币2elif(2)silverCoins
金币3ifgoldCoins
为什么用 elif 而不是三个 if?三个价值互斥,一枚硬币不可能既是金又是银。用 elif,一旦命中就跳过后续判断,省去无用比较;若写成三个独立 if,每枚硬币都会白白多比两次。逻辑虽也对,但不够优雅高效。

10.5 把数组传给函数

Passing Arrays to Functions

10.5.1 数组作参数(Arrays as Arguments)

10.5.1.1 分好的数组交给函数处理 Hand each bucket to a function

分拣完成后,三个数组各自交给对应的农民去捡。这里用了一个接收"农民 + 硬币数组"两个参数的函数 commandPeasant(peasant, coins):它在 coins 里找离该农民最近的硬币,命令农民移动过去。把整个数组当参数传进去,函数内部就能只在这一堆硬币里搜索,绝不会跑去捡别区的硬币。

# 函数接收一个农民和一个硬币数组
def commandPeasant(peasant, coins):
    coin = peasant.findNearest(coins)   # 只在这堆里找
    if coin:
        hero.command(peasant, "move", coin.pos)
别忘了判断 coin 是否存在:某一帧里,某类硬币可能一枚都没有,findNearest 会返回 None。所以 command 前一定要用 if coin: 确认真的找到了硬币,否则命令一个"不存在的目标"会报错。

10.5.2 字典管理单位(Managing Units with a Dict)

10.5.2.1 用名字对应农民 Map names to peasants

三个农民有各自的名字:Aurum(金)、Argentum(银)、Cuprum(铜,正是三种金属的拉丁名)。关卡用一个字典把名字映射到具体农民,取用时清晰明了。这样"金币数组交给 Aurum、银币数组交给 Argentum、铜币数组交给 Cuprum"就一目了然,把"分类的数据"和"处理的对象"整齐地对应起来。

# 用字典给三个农民起名对应
friends = hero.findFriends()
peasants = {
    "Aurum": friends[0],     # 金
    "Argentum": friends[1],  # 银
    "Cuprum": friends[2]     # 铜
}

# 分类数组各自交给对应农民
commandPeasant(peasants.Aurum, goldCoins)
commandPeasant(peasants.Argentum, silverCoins)
commandPeasant(peasants.Cuprum, bronzeCoins)

10.6 实战关卡精讲

Level Strategy & Analysis

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

第 16 关 · 资源谷

⛏️ 资源谷 —— 按价值分拣硬币,各归其主

扣哒世界 资源谷关卡 - koudashijie.com
图2:资源谷——金、银、铜三座山谷各产一种硬币,三个农民分别只能在自己的区域收集

关卡背景与目标

黄金峡谷、银色沼泽、青铜盆地三座山谷分别只生成金币(value 3)、银币(value 2)、铜币(value 1)。三个农民 Aurum、Argentum、Cuprum 各守一区,拿不到其他区域的硬币。目标是命令每个农民去捡"自己区域里最近的硬币",绝不能让他们瞄准错误的目标。

策略:先分拣,再分派

核心难点在于 findItems() 把所有硬币混在一个数组里返回。如果直接让农民对整个数组 findNearest,他可能瞄上别区的硬币而白跑一趟。正确策略分两步:第一步过滤——遍历所有硬币,按 value 分进金、银、铜三个数组;第二步分派——把每个分类数组连同对应农民传给 commandPeasant,让农民只在属于自己的那堆里找最近的。

思路拆解

这一关要你补两处。其一,补全过滤的分类:默认代码已写好金币分支 if item.value == 3: goldCoins.append(item),你需要照着加上 elif item.value == 2: silverCoins.append(item) 和 elif item.value == 1: bronzeCoins.append(item)。其二,补全 commandPeasant 函数体:用 peasant.findNearest(coins) 在传入的数组里找最近硬币,若找到就 hero.command(peasant, "move", coin.pos)。注意整个逻辑放在 while True 里,每帧重新扫描、重新分拣,因为硬币会不断刷新。

# 关键片段:每帧重扫、分拣、分派
while True:
    items = hero.findItems()
    goldCoins = []
    silverCoins = []
    bronzeCoins = []
    # 一次遍历分三类(银、铜两支需你补全)
    for item in items:
        if item.value == 3:
            goldCoins.append(item)
        elif item.value == 2:
            silverCoins.append(item)
        elif item.value == 1:
            bronzeCoins.append(item)
    # 分好的数组各自交给对应农民
    commandPeasant(peasants.Aurum, goldCoins)
    # ...银、铜同理
每帧重建空数组是关键:三个 = [] 必须写在 while 循环内部。若写在循环外,数组只会越积越多,混入早已被捡走的旧硬币,导致农民追逐已经不存在的目标。放在循环内、每帧清空重装,才能始终反映当前场上的硬币。
为什么要把数组传给函数?因为"找最近硬币并移动"这段逻辑,三个农民完全一样。把它封装成 commandPeasant(peasant, coins),只需换传入的农民和数组,就能复用同一段代码——这正是"过滤 + 函数封装"的组合威力:过滤负责把数据分好,函数负责统一处理每一份。
别让农民瞄错区:本关的陷阱就是"用整个 items 而不是分类后的数组"。一定要把 goldCoins 传给金区农民、silverCoins 传给银区农民,绝不能把没过滤的 items 传进去,否则农民会盯上够不着的别区硬币,原地空转。

10.7 常见错误

Common Mistakes

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

10.7.1.1 过滤筛选的典型问题 Typical issues with filtering

扣哒世界 过滤筛选常见错误速查 - koudashijie.com
图3:空数组建在循环外、忘判空、传错数组,是本单元最常见的失误
易错点清单:
  • 空数组建在循环外——goldCoins = [] 等必须在 while 内每帧重建,否则会不断堆积旧硬币。
  • 忘了 append——只写 if 判断却没把元素加进新数组,过滤结果永远是空的。
  • append 参数写错——应追加元素本身 append(item),别误写成 append(item.value) 或索引。
  • 用 = 代替 ==——判断相等要用 ==;写成 if item.value = 3 是赋值,会语法报错。
  • value 判断值弄反——金 3、银 2、铜 1,别把价值和数组对错(如把 value==2 追加到金币数组)。
  • 该用 elif 却全用 if——虽也能跑,但每枚硬币多做无用比较;互斥分类首选 if/elif。
  • 忘判空就 command——某类硬币可能为空,findNearest 返回 None,需先 if coin: 再命令。
  • 传错数组给函数——把未过滤的 items 或错误分类传给农民,导致他瞄准别区目标。

本单元小结 ✨

恭喜你打通了资源谷!这一单元你掌握了数据处理里最常用的一项技能——过滤筛选。你理解了过滤"保留或去除某些元素"的本质,牢记了它的三要素:遍历、条件、收集;你学会了用 for 遍历原数组、用 if 检查 item.value 这样的属性、用 append() 把合格元素追加进新数组;更进一步,你用 if/elif 在一次遍历里把硬币分成金、银、铜三类,再把每个分类数组作为参数传给 commandPeasant 函数,配合字典把三个农民和三类硬币整齐对应。过滤 + 函数封装 + 字典管理,三者组合让"给对的人分对的数据"变得清晰而高效。来回顾一下核心概念:

核心概念关键点对应内容
过滤的本质保留/去除符合条件的元素10.1
过滤三要素遍历 → 条件 → 收集10.1
遍历数组for item in items10.2
按属性筛选if item.value == 310.2 · 资源谷
新建空数组循环内每帧 [] 重建10.3 · 资源谷
append 追加数组.append(元素)10.3
if/elif 多路分类一次遍历分三类10.4 · 资源谷
数组作参数把分类数组传给函数10.5 · 资源谷
字典管理单位名字映射到农民10.5 · 资源谷
findNearest 判空if coin: 再命令10.5 · 资源谷

懂得把杂乱的数据按条件分门别类、再把每一份精准交给对的处理者的英雄,
才是云峰之上真正条理分明的程序员。继续向雪山之巅挺进吧,勇敢的程序员!🏔️⛏️💰

扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十单元

扣哒世界教研团队