freeCodeCamp 每日编程挑战解析:用 Python 求段落中最频繁的三个单词(Challenge 35: Word Frequency)
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本篇以 freeCodeCamp 开源仓库中「Dev Playground」超级块内每日编程挑战(Daily Coding Challenge)第 35 题「Word Frequency」为对象,完整拆解题目规格、官方测试用例与参考解法,并溯源该练习题从 Markdown 课程文件、GraphQL 抓取、数据库种子脚本到线上 API 路由的完整工程链路。读完本文,你既能独立写出通过全部测试的get_words实现,也能理解这道题在 freeCodeCamp 实际产品中是如何被分发与验证的。
挑战背景:这道题在仓库中的位置
「Word Frequency」是每日编程挑战系列的第 35 题,其唯一来源是课程 Markdown 文件 curriculum/challenges/english/blocks/daily-coding-challenges-python/68b06e589bf227324381476f.md,文件头部的 YAML 元数据定义了它的身份:
id: 68b06e589bf227324381476f:全局唯一挑战 ID,同时也是种子脚本写入 MongoDB 时使用的_id;title: "Challenge 35: Word Frequency";challengeType: 29:题目类型标记,用于客户端渲染与提交流程;dashedName: challenge-35。
在块定义文件 curriculum/structure/blocks/daily-coding-challenges-python.json 中,它排在daily-coding-challenges-python块的第 35 位,helpCategory为Python,blockLayout为legacy-challenge-list,并启用了usesMultifileEditor。该块隶属「Dev Playground」超级块,是每日挑战的内容源头;种子脚本 tools/daily-challenges/seed-daily-challenges.ts 中硬编码了EXPECTED_CHALLENGE_COUNT = 365,即这套每日挑战共 365 道,覆盖一整年。
值得一提的工程细节是:同一个挑战 ID 也存在于 JavaScript 块中,即 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68b06e589bf227324381476f.md。这意味着 Word Frequency 是一道双语言题目——JavaScript 与 Python 各有一份描述、测试与起始代码,种子脚本会按标题一一配对(详见后文「从练习题到线上功能」)。
题目规格:输入与输出的精确约定
原文档的--description--部分给出了如下完整需求:给定一段话(paragraph),返回一个由出现次数最多的三个单词组成的数组。
- 段落中的单词以空格分隔;
- 忽略大小写:例如
Hello与hello视为同一个单词; - 忽略标点:标点仅包括逗号(
,)、句点(.)和感叹号(!); - 返回数组中的所有单词必须是小写;
- 返回数组按出现次数降序排列,出现次数最多的单词排在最前。
约定函数签名为get_words(paragraph),参数为字符串,返回值为字符串数组。这组规则界定了三个核心加工步骤:清洗(去标点、统一小写)→ 分词(按空格拆分)→ 计数并排序取前三。
官方测试用例:如何验证你的实现
原文档--hints--提供了三个测试用例,每个测试都嵌在 JavaScript 的runPython调用中,借助 Python 标准库unittest.TestCase在浏览器端执行断言:
用例一:句子中出现多次编码主题词,验证计数与降序。
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(get_words("Coding in Python is fun because coding Python allows for coding in Python easily while coding"), ["coding", "python", "in"])`) }})用例二:句子含句点与感叹号,验证标点剥离与大小写归一。
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(get_words("I like coding. I like testing. I love debugging!"), ["i", "like", "coding"])`) }})用例三:标点密集、单词重复的句子,验证频率统计的稳定性。
({test: () => { runPython(` from unittest import TestCase TestCase().assertEqual(get_words("Debug, test, deploy. Debug, debug, test, deploy. Debug, test, test, deploy!"), ["debug", "test", "deploy"])`) }})三个用例覆盖了本题的核心边界:大小写混合、句点/逗号/感叹号同时出现、以及多个单词出现次数相同(用例二中coding、testing、love、debugging均只出现一次)时取最靠前的词。这种「测试字符串内嵌断言」的写法是 freeCodeCamp 挑战的标准形态——hints中的text与testString会被解析为课程测试节点,最终通过runPython在沙箱中执行 Python 代码。
起始代码:你需要补全的骨架
原文档--seed-contents--给出的起始代码如下,函数当前仅把参数原样返回:
def get_words(paragraph): return paragraph你的任务是把这段骨架补全为满足上述五条规则、能通过全部三个测试的完整实现。
参考解法:逐行拆解官方答案
原文档--solutions--提供了官方参考实现:
import re def get_words(paragraph): cleaned = re.sub(r'[.,?]', '', paragraph.lower()) words = cleaned.split() freq = {} for word in words: freq[word] = freq.get(word, 0) + 1 sorted_words = sorted(freq.keys(), key=lambda w: freq[w], reverse=True) return sorted_words[:3]逐行解析其设计:
- 第一步(清洗):
paragraph.lower()先统一小写(满足规则 2、4),随后re.sub(r'[.,?]', '', ...)用正则替换的方式把,.?全部删掉(规则 3)。使用re模块而非str.replace链式调用,是因为一次正则替换即可覆盖多个字符类,代码更简洁。 - 第二步(分词):
cleaned.split()以空白为分隔符切分单词(规则 1)。Python 的str.split()在不传参时会按任意连续空白切分并自动丢弃空串,因此即使出现多个连续空格也不会产生空词。 - 第三步(计数):遍历词表,用
freq.get(word, 0) + 1完成「读旧值、加一、写回」的计数惯用法,等价于collections.Counter的手写版。这里体现了字典get方法带默认值参数的典型应用。 - 第四步(排序):
sorted(freq.keys(), key=lambda w: freq[w], reverse=True)按键(单词)在字典中查频次作为排序键,降序排列。注意它排序的是去重后的键集合,而非原始词表,因此不会因为某词出现多次而重复计算。 - 第五步(截取):切片
[:3]取前三个最高频单词,对应「最频繁的三个单词」的题目要求。
复杂度上,该实现为O(n + m log m),其中n为段落单词总数(清洗与计数均为线性扫描),m为去重后的单词种类数(排序开销)。
一个值得留意的源码细节:标点集合与描述略有出入
对照题目描述(规则 3 声明标点仅为,.!)与官方解法中的正则[.,?],可以发现一个有趣的出入:正则实际剥离的是逗号、句点和问号,并不包含感叹号!。但第三个测试用例(结尾为deploy!)依然能够通过——因为剥离后词表为["debug","test","deploy","debug","debug","test","deploy","debug","test","test","deploy!"],其中deploy出现 2 次,deploy!仅 1 次,debug与test各 4 次;取前三时deploy(2 次)恰好压过deploy!(1 次)入围。也就是说,测试数据的设计让该差异没有暴露。编写自己的解法时,若严格按照描述剥离!(例如正则改为r'[.,!]'),同样能通过全部测试;这恰好说明以官方测试用例为准绳、同时遵循题目描述是最稳妥的策略。
并列情形的顺序保证
当多个单词频次相同(如用例二中的coding、testing、love、debugging都只出现 1 次)时,Python 的sorted是稳定排序,且本题的键源freq是普通字典(Python 3.7+ 保持插入顺序),因此并列单词按首次出现顺序保留,coding得以进入结果数组。这一点对理解预期输出至关重要。
进阶思考:解法的可替换实现
官方解法用字典 +sorted,思路直白、足够应付本题规模。若题目规模扩大或要求更严格的复杂度,可考虑以下变体(属扩展思考,非仓库内容):
- 用
collections.Counter(cleaned.split())一行完成计数,语义更清晰; - 用
heapq.nlargest(3, freq, key=freq.get)在词表很大时把排序降到O(n log 3)级别,只需维护大小为 3 的堆; - 将清洗正则收紧为
re.sub(r'[^\w\s]', '', paragraph.lower())可剥离所有非单词、非空白字符,泛化标点集合。
从练习题到线上功能:这道题在 freeCodeCamp 中的完整链路
Word Frequency 不只是静态练习题,它经由dev-playground超级块进入每日挑战产品线,形成了一条可完整追溯的工程链路:
- 内容源:挑战 Markdown 文件(即本题目所在文件)被
tools/daily-challenges的种子脚本消费。脚本 tools/daily-challenges/seed-daily-challenges.ts 通过 tools/daily-challenges/helpers.ts 中queryGraphQL向本地 Gatsby 客户端的 GraphQL 端点http://localhost:8000/___graphql发起查询,按superBlock: "dev-playground"、block: "daily-coding-challenges-python"(或-javascript)过滤,并标注「客户端需在运行中才能抓取到挑战数据」。 - 双语言配对:
combineChallenges函数(见 tools/daily-challenges/helpers.ts)将同一挑战号的 JavaScript 与 Python 版本配对,逐一校验标题、描述是否一致、测试数量是否相同,任一不匹配即抛错;随后以ObjectId(jsId)作为新文档_id,写入 MongoDB 的DailyCodingChallenges集合。 - 日期编排:脚本从
2025-08-11T00:00:00.000Z起逐日递增(START_DATE在脚本中有硬编码校验,防止意外修改),并为每条记录生成challengeNumber、date、title、description、javascript/python两组tests与challengeFiles(数据结构见 tools/daily-challenges/types.ts)。写入采用replaceOne + upsert的 bulkWrite 幂等策略,重复运行不会产生重复文档;运行前还会核对总数是否为 365。具体执行步骤见 tools/daily-challenges/README.md:复制sample.env为.env、安装依赖、启动开启即将上线功能的主客户端,然后在tools/daily-challenges下运行pnpm seed-daily-challenges。 - 线上分发:挑战数据由 API 服务对外提供。路由文件 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 定义了
GET /daily-coding-challenge/date/:date、/daily-coding-challenge/day/:day、/daily-coding-challenge/today、/daily-coding-challenge/month/:month、/daily-coding-challenge/all、/daily-coding-challenge/newest共六个公开接口,统一限制「不返回晚于美国中部时间当天」的题目,并对非法日期格式返回 400、未找到返回 404,同时埋点 Sentry 指标(如dcc.challenge_viewed、dcc.challenge_not_found)。值得注意,该插件注释明确说明这些接口只负责获取挑战信息,挑战提交仍走主挑战完成路由。 - 客户端展示:前端组件目录 client/src/components/daily-coding-challenge 下的
widget.tsx(当日挑战挂件)与calendar.tsx(日历视图)负责把按日期分发的挑战渲染给学习者,完成作答后进入课程进度体系。
相关文件速查
- 题目本体(Python):curriculum/challenges/english/blocks/daily-coding-challenges-python/68b06e589bf227324381476f.md
- 题目本体(JavaScript 同 ID 配对版):curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68b06e589bf227324381476f.md
- 块定义: curriculum/structure/blocks/daily-coding-challenges-python.json
- 种子脚本与配对逻辑:tools/daily-challenges/seed-daily-challenges.ts、tools/daily-challenges/helpers.ts、tools/daily-challenges/types.ts
- 分发 API:api/src/daily-coding-challenge/routes/daily-coding-challenge.ts
- 前端组件:client/src/components/daily-coding-challenge/widget.tsx、client/src/components/daily-coding-challenge/calendar.tsx
【免费下载链接】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),仅供参考