☰
freeCodeCamp 每日编程挑战 Challenge 28:罗马数字解析器的完整解法与原理剖析
2026/11/2 20:03:55 网站建设 项目流程

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 个符号构成,每个符号有固定取值:

符号值
I1
V5
X10
L50
C100
D500
M1000

核心换算规则只有一条,原文表述为:

从左到右读取数字。如果较小的数字出现在较大数字之前,则该值被减去;否则值被加上。

这条"小在左则减,小在右则加"的规则正是罗马数字减法记法(如 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; }

逐段拆解:

  1. romanMap查值表:一个普通对象充当符号到取值的映射,与题目表格一一对应。用对象下标代替switch/if-else链,是典型的"数据驱动"写法,后续若要扩展符号(本题不需要)只需加一行键值。

  2. for循环单遍扫描:i从 0 走到numeral.length - 1,对每个位置取出当前符号值current与下一个符号值next。整个算法只做一次线性遍历,时间复杂度 O(n)、空间复杂度 O(1)(n 为字符串长度)。

  3. 关键判断if (next && current < next):这里有两个隐含细节——

    • next先作为逻辑与的左操作数:当i指向最后一个字符时,numeral[i + 1]是undefined,romanMap[undefined]为undefined,next &&短路为假,自动落入else分支做加法,从而优雅处理了数组越界的边界条件,无需单独判断"是否最后一位";
    • current < next即规则表格里那句话的程序化表达:"较小数字出现在较大数字之前则减去"。减法记法中那个"小符号"本身被减去,它右边的大符号会在下一轮迭代中正常加上,因此 IV 得到-1 + 5 = 4,而不需要把 IV 当作整体处理。
  4. 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+10460
"DIV"500-1+5(见下文边界讨论)504
"MMXXV"1000+1000+10+10-1+52025

对应的断言代码即题目 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) 扫描之外额外增加一层基于规则的合法性检查,这属于本题范围之外的扩展方向。

解法在课程中的验证机制

从课程结构看,本题的验证由两部分组成:

  1. 测试用例驱动:hints 区块中每条assert.equal都会被平台转译为测试执行,函数返回值与期望整数逐条比对,全部通过即判定完成;
  2. 数据格式校验:每日挑战进入题库前需通过 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),仅供参考

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

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

立即咨询