☰
freeCodeCamp 每日编程挑战解析:用 Python 求段落中最频繁的三个单词(Challenge 35: Word Frequency)
2026/10/18 6:37:56 网站建设 项目流程

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),返回一个由出现次数最多的三个单词组成的数组。

  1. 段落中的单词以空格分隔;
  2. 忽略大小写:例如Hello与hello视为同一个单词;
  3. 忽略标点:标点仅包括逗号(,)、句点(.)和感叹号(!);
  4. 返回数组中的所有单词必须是小写;
  5. 返回数组按出现次数降序排列,出现次数最多的单词排在最前。

约定函数签名为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超级块进入每日挑战产品线,形成了一条可完整追溯的工程链路:

  1. 内容源:挑战 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)过滤,并标注「客户端需在运行中才能抓取到挑战数据」。
  2. 双语言配对:combineChallenges函数(见 tools/daily-challenges/helpers.ts)将同一挑战号的 JavaScript 与 Python 版本配对,逐一校验标题、描述是否一致、测试数量是否相同,任一不匹配即抛错;随后以ObjectId(jsId)作为新文档_id,写入 MongoDB 的DailyCodingChallenges集合。
  3. 日期编排:脚本从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。
  4. 线上分发:挑战数据由 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)。值得注意,该插件注释明确说明这些接口只负责获取挑战信息,挑战提交仍走主挑战完成路由。
  5. 客户端展示:前端组件目录 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),仅供参考

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

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

立即咨询