freeCodeCamp 每日编程挑战解析:用欧几里得算法求最小公倍数(LCM)
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本篇技术指南以 freeCodeCamp 开源课程仓库中的 "Challenge 103: LCM" 挑战文档(curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68ffb91507a5b645769328c6.md)为主体,讲解如何在 JavaScript 中实现最小公倍数(Least Common Multiple,LCM)算法,并以仓库源码为佐证,揭示这道题目从课程 Markdown 到线上做题环境的完整流转链路。读完本文,你将掌握 LCM 的数学定义与测试判定方法、欧几里得(辗转相除)算法求最大公约数的递归实现,并理解这类题目在 freeCodeCamp 平台中如何被校验、入库并被每日呈现给学习者。
挑战背景:它属于哪个体系
"Challenge 103: LCM" 并非独立的一道随堂测验,而是 freeCodeCamp 课程仓库中**每日编程挑战(Daily Coding Challenge)**序列的第 103 题。从课程结构文件 curriculum/structure/blocks/daily-coding-challenges-javascript.json 可以看到,它归属于daily-coding-challenges-javascript模块(block),其顺序表中登记为:
{ "id": "68ffb91507a5b645769328c6", "title": "Challenge 103: LCM" }结合仓库中的 superblock 结构文件 curriculum/structure/superblocks/dev-playground.json,可以推断该模块整体位于 "Dev Playground" 大模块之下,作为每日一题的内容源。该 block 配置同时标记了"isUpcomingChange": true、"helpCategory": "JavaScript"、"usesMultifileEditor": true等元信息,说明这类挑战基于多文件编辑器运行、按 JavaScript 帮助分类。
挑战文档自身的 frontmatter 则声明了challengeType: 28与dashedName: challenge-103,其中dashedName会被用于拼装学习页面的路由标识(前端代码中可见dashedName: challenge-${challengeNumber}的拼接逻辑,见 client/src/client-only-routes/show-daily-coding-challenge.tsx)。
题目要求与 LCM 的数学定义
原文档 --description-- 部分给出的任务陈述非常简洁:
Given two integers, return the least common multiple (LCM) of the two numbers.
即:给定两个整数,返回它们的最小公倍数。
文档同时对概念作了精确限定:
- LCM 是同时为两个数倍数的最小正整数;
- 例如输入
4和6,返回12,因为:4的倍数依次为4、8、12、…;6的倍数依次为6、12、18、…;12是同时能被两者整除的最小数。
这里的输入按文档措辞为两个整数(integer),因此题目实现需要留意负数的边界情形——这恰恰也是官方解法使用Math.abs的原因(详见下文解法剖析)。
从 GCD 挑战到 LCM 挑战:同一模块的知识递进
值得注意的是,第 103 题并非该模块中第一个数论题目。在模块顺序表中,第 97 题正是 "Challenge 97: GCD"(curriculum/structure/blocks/daily-coding-challenges-javascript.json 中登记 id 为68f6587287ad1f4ad39b0c83)。GCD 与 LCM 存在经典恒等关系:
lcm(a, b) × gcd(a, b) = |a × b|因此求解 LCM 最常见的策略就是先求最大公约数,再借助上式换算。这一递进设计让挑战者在连续几天内巩固数论算法,官方给出的 LCM 解法也明确复用了 GCD 思路。
判定标准:五组断言与验证方式
原文档的 --hints-- 部分给出了 5 个自动化断言,即实现必须通过的全部测试用例:
| 调用 | 期望返回 | 验证要点 |
|---|---|---|
lcm(4, 6) | 12 | 基础互质因子组合,4 与 6 的最大公约数为 2 |
lcm(9, 6) | 18 | 非互质但含较大质因子(9 的质因子含 3²) |
lcm(10, 100) | 100 | 一个数为另一个数的倍数时,LCM 等于较大者 |
lcm(13, 17) | 221 | 两个质数相乘,验证互质情形下 LCM 即两数之积 |
lcm(45, 70) | 630 | 稍大整数的普适性验证 |
这些断言在题目环境中会以类似下述代码执行(原文 68ffb91507a5b645769328c6.md 的 hints 块):
assert.equal(lcm(4, 6), 12); assert.equal(lcm(9, 6), 18); assert.equal(lcm(10, 100), 100); assert.equal(lcm(13, 17), 221); assert.equal(lcm(45, 70), 630);其中10与100、13与17两组用例尤其值得注意:前者覆盖了「成倍数关系」的退化情形,后者覆盖了「两数互质」的边界情形,是检验实现是否鲁棒的关键。
从数据模型看,这类挑战的测试在仓库中表示为{ text, testString }结构。校验器 client/src/utils/daily-coding-challenge-validator.ts 中定义了完整的结构约束:每个挑战须包含id、challengeNumber(不小于 1 的整数)、title、date、description,以及javascript、python两种语言的实现数据,而每种语言数据内必须含有tests数组与challengeFiles数组。这意味着本题的hints会被转写为数据库中javascript.tests的若干条testString,供在线编辑器逐条执行判定。
从种子代码到官方解法
种子代码(题目初始框架)
原文档 --seed-- 提供的是带缺失逻辑的函数骨架:
function lcm(a, b) { return a; }学习者只需补全lcm的内部实现,使函数最终返回两数的最小公倍数即可,函数签名lcm(a, b)保持不动。
官方解法:欧几里得 GCD + 乘积相除
原文档 --solutions-- 给出的参考实现如下:
function lcm(a, b) { function gcd(x, y) { return y === 0 ? x : gcd(y, x % y); } return Math.abs(a * b) / gcd(a, b); }这一实现可以拆成三个关键点逐层理解:
第一层:内嵌递归求 GCD。gcd(x, y)采用经典的欧几里得算法(辗转相除法):若y为0,则x即为最大公约数;否则递归调用gcd(y, x % y),把除数与余数作为新一轮参数。例如求gcd(6, 4):
x = 6, y = 4,y !== 0,递归gcd(4, 6 % 4 = 2);x = 4, y = 2,y !== 0,递归gcd(2, 4 % 2 = 0);x = 2, y = 0,返回2。
第二层:借助恒等式lcm(a, b) × gcd(a, b) = |a × b|换算。由于 LCM 与 GCD 的乘积恰好等于两数绝对值之积,直接用两数乘积除以 GCD 即可得到 LCM,无需暴力枚举倍数。
第三层:Math.abs(a * b)处理负数与符号问题。题目输入为整数,可能出现负数。若不做绝对值处理,负数的乘积除以 GCD 可能得到负数,与「最小公倍数为正整数」的定义冲突;同时若不先取绝对值,符号会干扰整除语义的可读性。官方实现通过Math.abs保证最终结果为正值。
以测试用例逐一手算验证:
lcm(4, 6):gcd(4, 6) = 2,|4 × 6| / 2 = 24 / 2 = 12✅lcm(9, 6):gcd(9, 6) = 3,54 / 3 = 18✅lcm(10, 100):gcd(10, 100) = 10,1000 / 10 = 100✅lcm(13, 17):互质,gcd = 1,221 / 1 = 221✅lcm(45, 70):gcd(45, 70) = 5,3150 / 5 = 630✅
可见该实现可完整通过全部 5 个断言。
延伸:不使用 GCD 的替代实现路径
除官方解法外,LCM 还有若干等价实现思路,可作为举一反三的练习方向(下述为通用算法知识,并非仓库内实现):
- 暴力递增法:从两数中较大者开始,每次增加较大者(步长可设为
max(a, b)),检查是否能同时整除两数,找到的第一个数即 LCM。思路直观,但当输入较大时效率明显偏低。 - 质因数分解法:将两数分别质因数分解,LCM 为各质因数取最高次幂之积。适合手工推导与教学演示,但编码复杂度和运行时开销都高于欧几里得方案。
- 基于 GCD 的库函数/内建函数:在支持内建 GCD 的环境(如 Python 的
math.gcd)中可直接套用同一恒等式。
对比可见,官方解法选用的「欧几里得递归求 GCD + 乘积相除」在时间复杂度和代码简洁度上都是较优的选择——欧几里得算法的时间复杂度约为O(log(min(a, b))),远优于暴力枚举。
源码级链路:挑战文档如何变成每天一道的练习题
理解题目本身之后,沿着仓库源码可以看到这条挑战从「课程 Markdown」到「在线可做」的完整流转链路,这有助于读者(尤其是希望自定义挑战或参与课程贡献的开发者)把握数据流:
第一步:从 GraphQL 拉取挑战。seed 脚本 tools/daily-challenges/seed-daily-challenges.ts 会读取 MongoDB 环境变量MONGOHQ_URL(默认mongodb://127.0.0.1:27017/freecodecamp?directConnection=true),并期望恰好找到 365 道挑战(脚本内EXPECTED_CHALLENGE_COUNT = 365),分别抓取 JavaScript 与 Python 两套实现(fetchChallenges('javascript')与fetchChallenges('python')),并校验两语言数量一致。
第二步:为挑战分配日期与编号。脚本从2025-08-11T00:00:00.000Z(START_DATE)起,为第i道挑战追加i × 24 小时,得到每天的发布日期;challengeNumber依序从1编号到365。由此,第 103 题在平台中对应一个具体日期,前端按日期路由取题。执行方式与依赖关系在 tools/daily-challenges/README.md 中有说明:运行前需先以「显示即将上线内容」的方式启动主客户端,使 GraphQL 能查询到该模块,再执行pnpm seed-daily-challenges,将数据写入DailyCodingChallenges集合(upsert 语义保证可重复执行)。
第三步:入库数据的结构约束校验。写入数据库的每条记录,其结构与 client/src/utils/daily-coding-challenge-validator.ts 中基于 Joi 定义的 schema 对齐:挑战须有challengeNumber(正整数)、date(日期字符串)、description等字段,javascript与python下又各含tests[{ text, testString }]与challengeFiles[{ fileKey, contents }]。LCM 题目的 5 条断言即以testString形式落库。
第四步:前端组装为经典挑战页面。client/src/client-only-routes/show-daily-coding-challenge.tsx 从数据库/API 取得挑战数据后,将其补全为通用挑战组件(ShowClassic)所需的 props:challengeType在 JavaScript 侧为28,superBlock: 'daily-coding-challenge',并注入challengeFiles(文件名为script、扩展名js、fileKeyscriptjs)与tests。这就是学习者网页上看到题目描述、编辑器和测试按钮的数据来源。
第五步:端到端验证。Playwright 测试 e2e/daily-coding-challenge.spec.ts 用 mock 数据模拟 API 返回(其结构与校验器 schema 完全一致,例如tests内含testString: 'assert.strictEqual(true, true);'),验证页面在合法日期下能正常加载,并能完成 JavaScript 与 Python 语言切换;对非法日期则会重定向到归档页。这为「LCM 题目页可正常打开、测试可运行」提供了自动化的质量保障。
本地验证建议
若想亲自验证 LCM 实现,无需启动整个平台,直接将函数与断言放入任意支持 ES 的 JavaScript 运行环境即可。参考下面的最小自测写法(仅作本地运行演示,仓库只读,勿修改课程文件):
function lcm(a, b) { function gcd(x, y) { return y === 0 ? x : gcd(y, x % y); } return Math.abs(a * b) / gcd(a, b); } const cases = [[4, 6, 12], [9, 6, 18], [10, 100, 100], [13, 17, 221], [45, 70, 630]]; for (const [a, b, expected] of cases) { const actual = lcm(a, b); console.log(`lcm(${a}, ${b}) => ${actual} (期望 ${expected}) ${actual === expected ? 'PASS' : 'FAIL'}`); }运行后如 5 行全部输出PASS,即代表实现与原文档的 5 个 hints 断言一致。也可顺手验证负数输入(例如lcm(-4, 6)应仍返回12),体会Math.abs存在的意义。
小结
"Challenge 103: LCM" 以一道简洁的数论函数题,串联起了三个层面的知识:最小公倍数的数学定义与测试驱动判定、欧几里得算法的递归实现与 LCM/GCD 恒等变换,以及 freeCodeCamp 仓库中每日编程挑战从 Markdown 课程文件经 seed 脚本、MongoDB、schema 校验到前端做题页面的完整工程链路。读者既可以把它当作一道独立的算法小题练习递归与边界处理,也可以顺着 tools/daily-challenges、client/src/utils/daily-coding-challenge-validator.ts 与 e2e/daily-coding-challenge.spec.ts 等文件,深入了解一个大型开源学习平台如何批量管理并发布每日练习内容。
【免费下载链接】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),仅供参考