freeCodeCamp 每日编程挑战 Challenge 28:罗马数字解析器的完整解法与原理剖析
【免费下载链接】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 28: Roman Numeral Parser" 为主体,完整讲解"将罗马数字字符串转换为十进制整数"这一经典算法题:从符号取值表、左减右加规则,到单遍前瞻算法的逐行实现、全部 7 个官方测试用例的推演验证,以及一个容易忽视的边界行为(非标准罗马串的处理)。读完后你可以独立编写并验证通过该挑战的完整parseRomanNumeral函数。
挑战背景:题目在课程中的位置与挑战格式
该题定义于课程库的每日编程挑战 JavaScript 区块中,题目文件为 challenge-28.md,在区块结构文件 daily-coding-challenges-javascript.json 的challengeOrder中登记为第 28 题(id 为68adce01c0e1144d0a90295c)。该区块声明了usesMultifileEditor: true与disableLoopProtectTests: true,即挑战在多文件编辑器中运行、且测试运行时不启用循环保护——对于本题这类一次线性遍历即可完成的小题,这避免了误伤长输入的性能误报。
题目文件采用 freeCodeCamp 统一的挑战 Markdown 结构:YAML frontmatter(id、title、challengeType: 28、dashedName)+ 四个标准区块:
# --description--:题目描述与规则;# --hints--:7 条带assert.equal代码示例的测试用例;# --seed--:初始代码骨架;# --solutions--:官方参考解法。
其中challengeType: 28对应课程 schema 中定义的题型编号,取值范围由 challenge-schema.js 中Joi.number().min(0).max(33)约束,题型常量统一维护在packages/shared的challenge-types配置中。
题目描述:规则与符号取值表
给定一个表示罗马数字的字符串,返回它对应的十进制整数值。
罗马数字由以下 7 个符号构成,每个符号有固定取值:
| 符号 | 值 |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
核心换算规则只有一条,原文表述为:
从左到右读取数字。如果较小的数字出现在较大数字之前,则该值被减去;否则值被加上。
这条"小在左则减,小在右则加"的规则正是罗马数字减法记法(如 IV=4、IX=9)的算法化表达。注意它把"识别 IV/IX/XC/CD/CM 等固定组合"的枚举思路,统一收敛为一个位置比较判断:只需比较相邻两个符号的大小,无需维护任何特例表。
初始代码骨架(Seed)
挑战给出的起点代码是一个未实现的函数壳,直接原样返回了输入:
function parseRomanNumeral(numeral) { return numeral; }任务就是替换函数体,使其对任意罗马数字串返回正确的整数。函数签名为parseRomanNumeral(numeral),入参是字符串,返回值为整数。
官方参考解法:单遍扫描 + 一步前瞻
题目文件# --solutions--区块给出的完整参考实现如下(可直接复制到编辑器运行):
function parseRomanNumeral(numeral) { const romanMap = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }; let total = 0; for (let i = 0; i < numeral.length; i++) { const current = romanMap[numeral[i]]; const next = romanMap[numeral[i + 1]]; if (next && current < next) { total -= current; } else { total += current; } } return total; }逐段拆解:
romanMap查值表:一个普通对象充当符号到取值的映射,与题目表格一一对应。用对象下标代替switch/if-else链,是典型的"数据驱动"写法,后续若要扩展符号(本题不需要)只需加一行键值。for循环单遍扫描:i从 0 走到numeral.length - 1,对每个位置取出当前符号值current与下一个符号值next。整个算法只做一次线性遍历,时间复杂度 O(n)、空间复杂度 O(1)(n 为字符串长度)。关键判断
if (next && current < next):这里有两个隐含细节——next先作为逻辑与的左操作数:当i指向最后一个字符时,numeral[i + 1]是undefined,romanMap[undefined]为undefined,next &&短路为假,自动落入else分支做加法,从而优雅处理了数组越界的边界条件,无需单独判断"是否最后一位";current < next即规则表格里那句话的程序化表达:"较小数字出现在较大数字之前则减去"。减法记法中那个"小符号"本身被减去,它右边的大符号会在下一轮迭代中正常加上,因此 IV 得到-1 + 5 = 4,而不需要把 IV 当作整体处理。
total累加器:以 0 起步,通过+=/-=逐步累积,最终返回。
七个官方测试用例逐一推演
# --hints--区块定义了 7 条assert.equal用例,覆盖了加法规则、单组减法、多组混合以及一个非标准输入。下面按算法逐格推演,验证参考解法全部通过:
| 输入 | 推演过程 | 期望输出 |
|---|---|---|
"III" | 1+1+1(三位都走加法分支,末位next为 undefined) | 3 |
"IV" | -1 + 5(I 后是更大的 V,触发减法分支) | 4 |
"XXVI" | 10+10-1+5(V 前的 X 后是更小的 V……实际顺序:X+X,X 后接 V 时 X<V 不成立故 +10,V 前是 X>V 故 -1,末位 +5) | 26 |
"XCIX" | -10+100-1+10(X 减、C 加、I 减、X 加) | 99 |
"CDLX" | -100+500+50-10+10 | 460 |
"DIV" | 500-1+5(见下文边界讨论) | 504 |
"MMXXV" | 1000+1000+10+10-1+5 | 2025 |
对应的断言代码即题目 hints 中的形式,例如:
assert.equal(parseRomanNumeral("III"), 3); assert.equal(parseRomanNumeral("IV"), 4); assert.equal(parseRomanNumeral("XXVI"), 26); assert.equal(parseRomanNumeral("XCIX"), 99); assert.equal(parseRomanNumeral("CDLX"), 460); assert.equal(parseRomanNumeral("DIV"), 504); assert.equal(parseRomanNumeral("MMXXV"), 2025);其中"XCIX"(99)是验证"多个减法记法可在一次遍历中独立叠加"的代表用例:XC 与 IX 两个减法片段之间没有任何状态依赖,算法天然兼容。"MMXXV"(2025)则覆盖千位 M 连写与末尾 V 的组合。
边界行为:"DIV"为什么等于 504
7 个用例中parseRomanNumeral("DIV")返回 504 是最值得玩味的一条。按严格罗马数字规范,DIV(500-1+5 的符号排列)并不是一个合法写法——标准规则要求减法记法只允许 I、X、C、M 作为被减数。但本题的参考解法不做合法性校验,它只是忠实地执行"逐位比较、小前大后则减"这一局部规则:
D(500)后接I(1):500 > 1,加法 →total = 500;I(1)后接V(5):1 < 5,减法 →total = 499;V(5)末位:加法 →total = 504。
由此可以推断,该挑战考察的是"实现换算算法"这一机械规则本身,而不是罗马数字的语法校验;输入保证来自题目描述中列出的 7 个符号。这也提醒我们:当后续遇到需要拒绝非法罗马串(如IIII、VV)的场景时,需要在该 O(n) 扫描之外额外增加一层基于规则的合法性检查,这属于本题范围之外的扩展方向。
解法在课程中的验证机制
从课程结构看,本题的验证由两部分组成:
- 测试用例驱动:hints 区块中每条
assert.equal都会被平台转译为测试执行,函数返回值与期望整数逐条比对,全部通过即判定完成; - 数据格式校验:每日挑战进入题库前需通过 Joi schema 校验。从 daily-coding-challenge-validator.ts 的源码结构看,每条挑战数据都要求
tests(含text与testString字段,正好对应 hints 的说明文本与断言代码)以及challengeFiles(fileKey+contents,对应 seed 代码)非空且为数组,challengeNumber为不小于 1 的整数。本题challengeType: 28的编号正落入该 schema 允许的取值范围。
e2e 测试 daily-coding-challenge.spec.ts 则从浏览器端验证了这类挑战在页面上的完整交互流程(加载、提交、结果反馈)。
小结
"Challenge 28: Roman Numeral Parser" 虽然题目短小,但浓缩了几个值得反复使用的算法与工程要点:
- 用符号值表 + 单遍前瞻把"左减右加"规则实现为 O(n)/O(1) 的线性算法;
- 用
next &&短路免写越界判断,末位自动落入加法分支; - 减法记法中的小符号"先减一次、大符号下轮再加",天然支持多个减法片段叠加(
XCIX推演); - 通过 7 条官方断言(3 / 4 / 26 / 99 / 460 / 504 / 2025)可完整验证正确性,其中
DIV → 504揭示了该题只要求实现局部换算规则、不校验罗马数字合法性的边界行为。
完整题目、测试与参考解法可直接在仓库中查阅:题目文件、区块顺序定义。
【免费下载链接】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),仅供参考