freeCodeCamp Python 每日编程挑战第 31 关 Array Diff:对称差集与字母排序完整解析
2026/9/11 20:47:38 网站建设 项目流程

freeCodeCamp Python 每日编程挑战第 31 关 Array Diff:对称差集与字母排序完整解析

【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp

本篇技术指南围绕 freeCodeCamp 课程仓库中daily-coding-challenges-python区块的第 31 个挑战Array Diff展开,完整拆解题目语义、5 个官方测试用例的判定逻辑、种子代码的改造路径与参考解法,并结合 挑战源文件、API 路由实现 与 种子脚本 说明这类挑战从 Markdown 文档到数据库记录、再到每日投递与自动判题的全链路机制。读完本文,你将掌握对称差集(symmetric difference)的多种 Python 实现、去重与排序的边界处理,以及 freeCodeCamp 每日编程挑战体系的源码级工作原理。

挑战定位:每日编程挑战系列中的一环

在 freeCodeCamp 课程仓库中,每日编程挑战(Daily Coding Challenge)由 JavaScript 与 Python 两套平行区块组成,分别位于daily-coding-challenges-javascriptdaily-coding-challenges-python。区块结构文件 记录了该区块的元信息:

  • isUpcomingChange: true——该区块作为即将上线的新内容维护,需要通过环境变量开启展示;
  • usesMultifileEditor: true——挑战使用多文件编辑器环境;
  • helpCategory: "Python"——归类到 Python 帮助分类;
  • blockLayout: "legacy-challenge-list"——采用传统挑战列表布局。

Array Diff 是该区块的第 31 个挑战,其挑战文档的 frontmatter 标注为:

  • id: 68af0687ef34c76c28ffa549——全局唯一挑战 ID,也是挑战完成记录与数据库中记录关联的主键;
  • title: "Challenge 31: Array Diff"
  • challengeType: 29——对应多文件编辑器的挑战类型;
  • dashedName: challenge-31

在 区块结构文件 的challengeOrder中,它紧随 Challenge 30: Unique Characters,后接 Challenge 32: Reverse Sentence,是整套每日挑战难度递进序列中的一环。

题目语义拆解:只出现在一个数组中的值

挑战的--description--部分给出了核心需求:

Given two arrays with strings values, return a new array containing all the values that appear in only one of the arrays. The returned array should be sorted in alphabetical order.

翻译为两层约束:

  1. 集合语义:给定两个仅含字符串的数组,返回"只出现在其中一个数组"的所有值。这正是数学上的对称差集(symmetric difference,记作 A △ B):(A − B) ∪ (B − A),即属于 A 但不属于 B、以及属于 B 但不属于 A 的元素。
  2. 输出排序:返回数组必须按字母顺序(alphabetical order)排序。

需要注意题目的措辞 "appear in only one of the arrays"——它关注的是"值是否存在",因此重复元素也是隐含考察点:参考解法首先对两个输入数组分别去重,再去求对称差。这从 挑战源文件 的--solutions--段可以清楚看到:

def array_diff(arr1, arr2): unique1 = [] for v in arr1: if v not in unique1: unique1.append(v) unique2 = [] for v in arr2: if v not in unique2: unique2.append(v) only1 = [v for v in unique1 if v not in unique2] only2 = [v for v in unique2 if v not in unique1] return sorted(only1 + only2)

参考解法分为三步:

  1. 分别去重:用v not in unique1的线性查找方式,把arr1arr2各自折叠成无重复序列unique1unique2
  2. 双向过滤only1保留unique1中不在unique2的值,only2保留unique2中不在unique1的值,二者合并即为对称差集;
  3. 排序返回sorted(...)对合并结果做字母序排序。

去重这一步是解题的关键:若不去重,对于含重复值的输入,输出中会残留重复项,无法精确满足"值只出现在一个数组中"的集合语义。

官方测试用例逐一验证

挑战源文件 的--hints--段内置了 5 个由runPython+unittest.TestCase().assertEqual执行的测试,它们共同界定了函数必须满足的行为:

输入 arr1输入 arr2期望输出覆盖要点
["apple", "banana"]["apple", "banana", "cherry"]["cherry"]仅 arr2 多出的值应被返回
["apple", "banana", "cherry"]["apple", "banana"]["cherry"]仅 arr1 多出的值应被返回(方向对称)
["one", "two", "three", "four", "six"]["one", "three", "eight"]["eight", "four", "six", "two"]多元素差集 + 输出必须字母排序
["two", "four", "five", "eight"]["one", "two", "three", "four", "seven", "eight"]["five", "one", "seven", "three"]输出与输入顺序完全无关,只按字母序
["I", "like", "freeCodeCamp"]["I", "like", "rocks"]["freeCodeCamp", "rocks"]区分大小写:大写I与小写单词不混淆

第 3、4 个用例重点校验排序:例如第三个用例中两数组的公共元素是"one""three",差集元素为"two""four""six""eight",期望输出按字母序为["eight", "four", "six", "two"],与输入数组中的出现顺序完全不同。第 5 个用例验证字符串比较是区分大小写的,"I"不会被当作与"like"之类的小写词相同。

在课程测试体系中,这些 hint 会被解析为text(展示给学习者的说明文本)与testString(实际执行的判定代码),对应 Prisma 模型中DailyCodingChallengeApiLanguageTests的两个字段,详见下文"从 Markdown 到数据库"一节。

从种子代码出发:最小改造思路

挑战的--seed--段给出了初始代码,学习者需要在此骨架之上完成实现:

def array_diff(arr1, arr2): return arr1

种子代码目前直接返回arr1,显然无法通过任何用例。一个保持官方解法思路、但对学习者更友好的写法是在原函数内完成"去重—求差—排序"三步:

def array_diff(arr1, arr2): unique1 = list(dict.fromkeys(arr1)) # 保持顺序去重 unique2 = list(dict.fromkeys(arr2)) only1 = [v for v in unique1 if v not in unique2] only2 = [v for v in unique2 if v not in unique1] return sorted(only1 + only2)

dict.fromkeys利用了字典键的唯一性,同时保留元素首次出现的顺序;由于最终结果要排序,顺序保持并非必需,但能让中间步骤更直观。官方参考解法使用的v not in unique1线性查找同样成立,二者都能通过全部 5 个测试。

进阶方案:用集合运算实现对称差集

既然题目本质是求对称差集,Python 内置的set类型提供了两种直接表达方式:

def array_diff(arr1, arr2): s1, s2 = set(arr1), set(arr2) return sorted(s1 ^ s2) # 对称差集运算符

或使用集合方法:

def array_diff(arr1, arr2): return sorted(set(arr1).symmetric_difference(arr2))

这两种写法更简洁,且set的哈希查找让整体复杂度从参考解法的 O(n·m) 降到 O(n + m)。需要注意一个行为差异:set会同时完成去重与去顺序,因此当输入含重复元素时,集合方案天然满足"值只出现一次"的集合语义,与官方参考解法的去重意图一致。

复杂度对比

方案去重方式差集计算时间复杂度空间复杂度
官方参考解法线性查找(in于列表)双向列表过滤O(n·m)(n、m 为两数组长度)O(n + m)
dict.fromkeys+ 列表过滤字典键去重双向列表过滤O(n + m) 去重 + O(n·m) 过滤O(n + m)
set对称差集哈希集合^运算符O(n + m)O(n + m)

对于字符串数组且规模不大的每日挑战场景,三种方案均能通过全部测试;官方解法刻意避开set,可能是为了让学习者练习列表推导与去重逻辑,这一点从源码可以确认——挑战源文件 的参考解法全程只使用列表与sorted

常见陷阱与边界情况

结合题目约束与测试用例,容易踩坑的点包括:

  • 忘记排序:只返回差集而不调用sorted(),会直接导致第 3、4 个用例失败——期望输出严格按字母序。
  • 忘记去重:若输入数组含重复值(题目未禁止),不去重会产生重复输出,破坏集合语义。
  • 方向遗漏:只计算arr1 - arr2而忽略arr2 - arr1,会让第 1 个用例("cherry"只在 arr2)返回空数组。
  • 大小写混淆:字符串比较区分大小写,"I""i"是不同值。
  • 原地修改输入sorted()返回新列表,若误用list.sort()会修改原数组的引用;虽然测试只断言返回值,但保持函数无副作用是良好实践。

源码级原理:挑战如何从 Markdown 变成每日投递

Array Diff 这类挑战并不是简单静态页面,而是经由"课程内容 → GraphQL → 种子脚本 → MongoDB → API → 客户端组件"的完整流水线呈现在学习者面前。

数据库模型

Prisma Schema 定义了DailyCodingChallenges模型:

model DailyCodingChallenges { id String @id @default(auto()) @map("_id") @db.ObjectId challengeNumber Int date DateTime title String description String javascript DailyCodingChallengeApiLanguage python DailyCodingChallengeApiLanguage } type DailyCodingChallengeApiLanguage { tests DailyCodingChallengeApiLanguageTests[] challengeFiles DailyCodingChallengeApiLanguageChallengeFiles[] }

每条记录同时携带 JavaScript 与 Python 两个语言版本(tests+challengeFiles),date字段决定挑战在哪一天对外可见——这正是"每日"机制的落点。

种子脚本与语言一致性校验

tools/daily-challenges/seed-daily-challenges.ts 负责把课程内容灌入数据库:它从运行中的客户端 GraphQL 端点(http://localhost:8000/___graphql)按superBlock: "dev-playground"block: "daily-coding-challenges-python"(以及 JavaScript 版本)拉取全部挑战,逐条配对生成记录。脚本内置了严格的一致性断言:

  • 期望挑战总数为 365(EXPECTED_CHALLENGE_COUNT),JS 与 Python 数量必须相等;
  • 首条挑战的日期被固定为2025-08-11,之后每天递增一天,且脚本会在起始日期被意外改动时直接抛错,防止生产环境已发布内容错位;
  • helpers.ts 的combineChallenges在合并时校验 JS/Python 两版的titledescriptiontests数量完全一致,任一不匹配即拒绝写入;
  • 记录_id直接取用 JS 挑战的 id(如本文挑战的68af0687ef34c76c28ffa549),保证挑战完成记录与课程内容可互相追踪。

与之对应的课程级校验在 curriculum/src/test/daily-challenges.test.js:通过getChallenges(CURRICULUM_LOCALE, { superBlock: 'dev-playground' })加载挑战后,逐一断言 JS/Python 两个区块的挑战数量、ID、标题、描述与测试数量匹配。

API 投递与客户端入口

挑战对外暴露的读取接口位于 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts,共 6 个 GET 端点:

端点参数格式行为
/daily-coding-challenge/date/:dateYYYY-MM-DD按具体日期取挑战
/daily-coding-challenge/day/:dayMM-DD按月-日取(自动匹配到对应年份)
/daily-coding-challenge/today返回美国中部时间当天的挑战
/daily-coding-challenge/month/:monthYYYY-MM返回某月挑战摘要列表
/daily-coding-challenge/all返回全部已发布挑战摘要
/daily-coding-challenge/newest返回最新挑战日期

值得注意的约束:所有端点都只返回"今天(美国中部时间)之前"的挑战(challenge.date > getUtcMidnight(getNowUsCentral())会被 404 拦截),保证未发布内容不可见。请求参数的模式校验由 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts 中的 TypeBox Schema 完成,例如日期必须是YYYY-MM-DD格式、单条响应的id必须是 24 位 ObjectId。每条成功/失败记录还会通过 Sentry metrics 上报dcc.challenge_vieweddcc.challenge_not_founddcc.request_failed等指标。

客户端侧,widget.tsx 中的DailyCodingChallengeWidget组件在课程地图与首页渲染两个入口按钮:进入"今日挑战"(链接由getMonthDayUsCentral()生成MM-DD格式)以及进入历史档案库。学习者在页面中编写array_diff并通过全部 5 个 hint 后,提交完成记录走的是主挑战完成流程(API README 明确说明"Daily challenge submission still lives in the main part of the API")。

总结

Array Diff 看似是一个简单的双数组求差题,但它在 freeCodeCamp 的每日编程挑战体系中承载了完整的学习闭环:理解对称差集的集合语义、掌握去重与字母排序的边界、对比set与列表推导两种实现风格。透过 挑战源文件 与 区块结构文件、Prisma 模型、API 路由、种子脚本 与 课程测试 的源码证据,还能看到一道 Markdown 挑战如何被校验、入库、按日投递并被自动判题——这正是把"刷题"升级为"理解平台机制"的进阶视角。

【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询