freeCodeCamp 每日编码挑战 321:Periodic Spelling 元素周期表拼词算法解析
2026/9/10 19:20:58 网站建设 项目流程

freeCodeCamp 每日编码挑战 321:Periodic Spelling 元素周期表拼词算法解析

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

本篇技术指南以 freeCodeCamp 开源仓库中curriculum/challenges/english/blocks/daily-coding-challenges-javascript板块下的Challenge 321: Periodic Spelling(挑战文件 6a15cadf5f240d05a264955f.md)为核心,完整讲解题目约束、全部测试用例、官方参考解中的 Map 哈希与递归回溯实现,并沿仓库源码链路展示这道"每日一题"从课程 Markdown 到数据库种子脚本、再到前端组件的完整落地方式。读完本文,你将掌握一类"字典单词拆分"问题的通用解法,并理解 freeCodeCamp 每日编码挑战在仓库中的生产化流程。

一、挑战概述:用元素周期表符号拼写单词

Challenge 321 是 freeCodeCamp 每日编码挑战(Daily Coding Challenge)JavaScript 系列中的第 321 题,题目要求实现函数getPeriodicSpelling(word)

Given a word, determine if it can be spelled using element symbols from the periodic table.

给定一个单词,判断它能否用元素周期表中的元素符号拼写出来。核心约束如下:

  • 忽略大小写:输入单词的大小写不影响判定,例如"neon"可以拆解为符号"Ne""O""N"
  • 返回原始大小写:结果数组中的元素符号必须保持其在周期表中的原始大小写形式(首字母大写、第二个字母小写),并按拼写顺序排列;
  • 不可拼写返回空数组:若单词无法由元素符号完整拼出,返回[]

题目附带了完整的 118 个元素符号列表(从"H""He""Og"),这既是解题所需的"字典",也决定了题目的两个关键特征:

  1. 单字母与双字母符号并存:既有"H""C""O"这样的单字母符号,也有"He""Ne""Ti"等双字母符号,因此同一单词可能存在多种拆分方式;
  2. 映射表固定且无重复:每个符号在列表中是唯一的,可以直接用于建立大小写无关的查找结构。
["H","He","Li","Be","B","C","N","O","F","Ne","Na","Mg","Al","Si","P","S","Cl","Ar","K","Ca","Sc","Ti","V","Cr","Mn","Fe","Co","Ni","Cu","Zn","Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y","Zr","Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn","Sb","Te","I","Xe","Cs","Ba","La","Ce","Pr","Nd","Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb","Lu","Hf","Ta","W","Re","Os","Ir","Pt","Au","Hg","Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th","Pa","U","Np","Pu","Am","Cm","Bk","Cf","Es","Fm","Md","No","Lr","Rf","Db","Sg","Bh","Hs","Mt","Ds","Rg","Cn","Nh","Fl","Mc","Lv","Ts","Og"]

挑战文件的--seed--节给出了初始脚手架:函数签名function getPeriodicSpelling(word) { return word; },学习者需要在此基础上补充算法实现。

二、测试用例逐条拆解:单一解与多重解并存

挑战的--hints--节共包含 8 条断言,是理解题目意图的最佳入口。前 3 条是单一解情形,后 5 条则引入了多重解(分支回溯)与无解情形:

输入期望输出特点
"neon"["Ne", "O", "N"]单一解,验证大小写归一化
"rational"["Ra", "Ti", "O", "N", "Al"]单一解,全部为双字母/单字母混合
"yarn"["Y", "Ar", "N"]单一解
"carbon"["C", "Ar", "B", "O", "N"]["Ca", "Rb", "O", "N"]多重解,任选其一
"noisy"["N", "O", "I", "S", "Y"]["No", "I", "S", "Y"]多重解
"bicycles"["B", "I", "C", "Y", "Cl", "Es"]["Bi", "C", "Y", "Cl", "Es"]多重解
"optics"["O", "P", "Ti", "C", "S"]["O", "P", "Ti", "Cs"]["O", "Pt", "I", "C", "S"]["O", "Pt", "I", "Cs"]四种合法解
"value"[]无解,返回空数组

多重解的断言方式值得注意:hints 并没有要求返回"字典序最小"或"符号数最少"的解,而是通过JSON.stringify后与多个合法路径逐一比对(如carbonpath1/path2),只要命中任意一条合法拆分即通过。这大大放宽了实现自由度——递归时优先取双字母还是单字母都不会影响判题结果,只要回溯逻辑正确即可。

const result = JSON.stringify(getPeriodicSpelling("optics")); const path1 = JSON.stringify(["O", "P", "Ti", "C", "S"]); const path2 = JSON.stringify(["O", "P", "Ti", "Cs"]); const path3 = JSON.stringify(["O", "Pt", "I", "C", "S"]); const path4 = JSON.stringify(["O", "Pt", "I", "Cs"]); assert.isTrue(result === path1 || result === path2 || result === path3 || result === path4);

"value"返回[]的用例则专门考察失败路径的终止条件:递归必须在所有拆分尝试均告失败时返回空数组(而不是抛错或返回中间状态)。

三、问题本质:一类"字典单词拆分"问题

将题目抽象后可以发现,它本质上是经典的Word Break(单词拆分)问题的变体,只是"字典"变成了 118 个元素符号,且每个符号长度只能是 1 或 2:

  • 输入是长度为n的字符串(小写归一化后);
  • 在每一步,游标i处可以尝试取长度为12的前缀(因为周期表符号最短 1 个字母、最长 2 个字母);
  • 若该前缀是合法元素符号,则递归处理剩余部分word[i+1:]word[i+2:]
  • 递归终点是i === word.length,即整个单词恰好被符号序列覆盖完毕;
  • 若某条路径走不通,需要回溯到上一个决策点尝试另一种切分。

由于符号只有 1 或 2 两种长度,每个位置的分支因子最多为 2,因此递归树是二叉树形态。若不考虑剪枝,朴素递归的最坏情况时间复杂度为 O(2^n),但随着字符串变长,前缀查表会很快失败,实际分支远少于理论值。题目的输入为普通英文单词(长度通常不超过 12),参考解直接采用带失败返回的递归即可通过全部测试。

四、官方参考解:Map 哈希 + 递归回溯

挑战文件的--solutions--节提供了官方参考实现,其核心是Map建立"小写符号 → 原始符号"的哈希映射,再以递归回溯完成拆分:

function getPeriodicSpelling(word) { const elements = ["H","He","Li","Be","B","C","N","O","F","Ne","Na","Mg","Al","Si","P","S","Cl","Ar","K","Ca","Sc","Ti","V","Cr","Mn","Fe","Co","Ni","Cu","Zn","Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y","Zr","Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn","Sb","Te","I","Xe","Cs","Ba","La","Ce","Pr","Nd","Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb","Lu","Hf","Ta","W","Re","Os","Ir","Pt","Au","Hg","Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th","Pa","U","Np","Pu","Am","Cm","Bk","Cf","Es","Fm","Md","No","Lr","Rf","Db","Sg","Bh","Hs","Mt","Ds","Rg","Cn","Nh","Fl","Mc","Lv","Ts","Og"]; const lower = new Map(elements.map(e => [e.toLowerCase(), e])); function spell(word, i) { if (i === word.length) return []; const one = word.slice(i, i + 1); const two = word.slice(i, i + 2); if (lower.has(two)) { const rest = spell(word, i + 2); if (rest !== null) return [lower.get(two), ...rest]; } if (lower.has(one)) { const rest = spell(word, i + 1); if (rest !== null) return [lower.get(one), ...rest]; } return null; } return spell(word.toLowerCase(), 0) ?? []; }

逐行解读这段参考实现:

  1. 映射构建(O(118))elements.map(e => [e.toLowerCase(), e])将全部符号转为小写键,值保留原始大小写。这样lower.get("ne")返回"Ne",天然满足"返回原始大小写"的需求,也免去了对输入单词逐个字符做大小写判断;
  2. 递归函数spell(word, i)i是当前读取游标。终止条件i === word.length返回[]作为成功基底;
  3. 优先尝试双字母two = word.slice(i, i + 2),若命中lower则递归推进 2 个字符。只有递归返回非null时才拼接结果并返回,这正是回溯的关键——双字母路径失败时不会污染结果;
  4. 再尝试单字母:双字母失败后回退到one = word.slice(i, i + 1)分支,同样先递归后拼接;
  5. 失败信号用null:两条路径都走不通时返回null,由?? []在入口处统一转换为空数组。??(空值合并)保证了[](真值语义上的"成功但为空")不会被误替换。

该实现的精巧之处在于:返回值本身兼作"成功标志"与"路径记录"——null表示失败,数组表示成功路径,从而省去了单独维护visitedpath栈的开销。由于题目不要求返回所有解或最优解,找到任意一条可行路径即可提前回溯返回。

五、算法复杂度与边界讨论

  • 时间复杂度:每次调用至多进行 2 次Map.has()查询(每次 O(1))与 2 次递归调用,递归深度为 O(n)。最坏情况(如全由单字母符号组成的路径,例如"noy"["N","O","Y"])下,递归树节点数为 O(2^n),但哈希查询失败会立即剪枝,实际英文单词场景下远低于该上界。若需处理超长输入,可引入记忆化(memoization):用Map<number, string[] | null>缓存i位置的失败/成功结果,将复杂度降至 O(n);
  • 空间复杂度:递归栈深度 O(n),符号映射表 O(118) 常数空间;
  • 边界情况
    • 空字符串:spell("", 0)立即命中i === word.length,返回[],可视为"空单词可拼写"的平凡情形;
    • 含数字或特殊字符的输入:既不在单字母也不在双字母表中,两条路径均失败,最终返回[]
    • 大小写混输入如"NeON":入口统一toLowerCase()归一化,不影响判定。

值得补充的是,hints 中"bicycles"的路径["B", "I", "C", "Y", "Cl", "Es"]用到了符号"Es"(锿),"optics"的四种合法解用到了"Pt"(铂)、"Ti"(钛)、"Cs"(铯),说明题目有意覆盖了双字母符号与单字母符号交替、且存在"贪心优先双字母会走错"的情形——例如"carbon"若贪心取双字母"Ca"后剩余"rbon""rb""r"均不是合法符号,必须回溯改取"C"。这正是考验回溯能力的设计点。

六、从单道题到产品:每日挑战在仓库中的完整链路

Challenge 321 并非孤立存在的课程 Markdown,它在 freeCodeCamp 仓库中有一条完整的生产链路,理解它有助于把这道题放进真实项目的上下文中:

6.1 课程板块定义

挑战所属板块由 daily-coding-challenges-javascript.json 定义,该文件以challengeOrder数组登记了全部挑战(Challenge 1: Vowel Balance 起,按天递增),其中 JavaScript 与 Python 两个板块一一对应,challengeType标识了每日挑战的专用类型。

6.2 数据库种子脚本

seed-daily-challenges.ts 负责把每日挑战写入 MongoDB 的DailyCodingChallenges集合:

  • 通过 helpers.ts 中的fetchChallenges('javascript' | 'python')函数,向本地 Gatsby 客户端暴露的 GraphQL 端点http://localhost:8000/___graphql查询dev-playgroundsuperblock 下的daily-coding-challenges-javascript板块,并按challengeOrder升序取出descriptionteststestString+text)、challengeFiles等字段;
  • combineChallenges将同日期的 JS 与 Python 挑战配对,校验标题、描述、测试数量一致后,以 JS 挑战的id作为 MongoDB 文档_id,写入包含challengeNumberdatejavascriptpython双语言的统一文档;
  • 脚本硬性断言挑战总数为365,起始日期为2025-08-11T00:00:00.000Z,此后每天递增一天,并注释强调发布后不可更改起始日期(这是每日挑战"每日一题"节奏的正确性保证)。

运行方式在 tools/daily-challenges/README.md 中有明确说明:复制sample.env.env、安装依赖、以"显示即将上线内容(upcoming changes shown)"模式启动主客户端,然后在tools/daily-challenges目录执行pnpm seed-daily-challenges完成播种。

6.3 API 与前端呈现

  • API 侧:daily-coding-challenge.ts 与其测试文件 daily-coding-challenge.test.ts 提供按日期读取挑战数据的服务端接口;
  • 前端侧:widget.tsx、calendar.tsx 及 helpers.ts 实现挑战展示组件与日期计算逻辑;
  • E2E 侧:daily-coding-challenge.spec.ts 覆盖了/learn/daily-coding-challenge/08-11等日期路由、无效日期重定向到 archive、以及通过种子命令构造测试数据等端到端场景。

这意味着 Challenge 321 这类题目的 Markdown 是"单一事实来源":它既驱动学习者页面上的题目与测试运行,又被种子脚本转化为按日期发布的每日挑战数据,最终通过 API 呈现给用户。对学习者而言,在本地运行pnpm seed-daily-challenges后即可在/learn/daily-coding-challenge/<日期>页面实际作答第 321 题。

七、总结:从拼词题到通用解题思维

Challenge 321: Periodic Spelling 表面是一道"元素符号拼词"趣味题,实际考察的是三类核心能力:

  1. 哈希映射的设计:把 118 个符号用小写键建立Map,同时解决大小写归一化与"还原原始符号大小写"两个需求,是空间换时间的典型手法;
  2. 递归回溯的终止与信号设计:用null作为失败信号、以返回值同时承载"是否成功"与"路径内容",并用?? []兜底,是简洁且不易出错的分治写法;
  3. 多解宽容的判题设计:hints 允许多条合法路径中的任意一条,启示我们在实现时不必追求"唯一最优解",正确回溯 + 任意可行解即可通过验证。

掌握这类"固定长度词元 + 查表 + 回溯"的套路后,你可以把它直接迁移到其他场景——如 IP 地址/电话号码分段、罗马数字解析、以及经典 Word Break 问题。结合 官方参考解 动手实现一遍,再对比仓库中每日挑战的完整生产链路,既能巩固算法功底,也能理解开源课程内容如何被工程化地构建、测试与发布。

【免费下载链接】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),仅供参考

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

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

立即咨询