LeetCode 12. 整数转罗马数字:从逐位实现到规则复用优化

14 阅读6分钟

罗马数字作为古老的计数体系,其转换规则围绕「加法优先、特殊减法补充」设计,LeetCode 第 12 题要求我们将 1~3999 范围内的整数转换为罗马数字,核心是精准适配罗马数字的符号规则。本文将从基础逐位实现入手,再到规则复用优化,逐步拆解两种实现思路的逻辑的逻辑,同时点明关键易错点。

一、题目核心规则梳理

首先明确罗马数字的符号与转换规则,避免实现时偏离需求:

1. 基础符号映射

符号符号
I1L50
V5C100
X10D500
M1000--

2. 转换核心规则

  • 特殊减法规则:仅支持 4(IV)、9(IX)、40(XL)、90(XC)、400(CD)、900(CM) 六种减法形式,对应值以 4 或 9 开头的场景。

  • 加法规则:非 4/9 开头的值,优先选最大可减符号累加,且 I、X、C、M(10 的次方)最多连续累加 3 次,V、L、D 不可连续累加。

  • 范围限制:输入整数满足 1 ≤ num ≤ 3999,千位最大值为 3,无 5000 及以上符号。

二、基础实现:逐位拆解法(intToRoman_1)

这种思路的核心是「将整数按千、百、十、个位拆分,逐位适配罗马数字规则」,逻辑直观,贴合规则理解,适合新手入门。

1. 实现逻辑拆解

步骤 1:拆分每一位数值

通过取余和整除运算,分别提取千、百、十、个位的数值,注意用 Math.floor 确保结果为整数(避免浮点数干扰):


const oneNum = Math.floor(num % 10); // 个位
const tenNum = Math.floor((num % 100) / 10); // 十位
const hundredNum = Math.floor((num % 1000) / 100); // 百位
const thousandNum = Math.floor(num / 1000); // 千位

步骤 2:逐位转换拼接

从高位到低位(千 → 个)转换,每一位遵循「先判断特殊减法,再处理加法」的逻辑:

  • 千位:仅需累加 M(因最大值为 3),无需处理减法和 5 倍符号。

  • 百位/十位/个位:先判断是否为 9(对应 CM/XC/IX),再判断是否为 4(对应 CD/XL/IV),接着判断是否 ≥5(对应 D/L/V 加后续累加),最后累加基础符号(C/X/I)。

2. 代码优势与不足

优势

逻辑直白,完全贴合罗马数字规则,每一步转换都可对应到具体规则,调试和理解成本低,适合快速上手实现。

不足

代码冗余严重:百位、十位、个位的转换逻辑高度重复,仅符号不同,后续维护需修改多处代码,扩展性差。

三、优化实现:规则复用版(intToRoman_2)

针对基础版的冗余问题,核心优化思路是「抽象统一规则,复用处理逻辑」,将重复的逐位判断抽象为规则表,用一次循环完成所有位的转换。

1. 实现逻辑拆解

步骤 1:定义统一规则表

将每一位的「基础符号、5 倍符号、9 倍符号、4 倍符号、位权」封装为对象,按「个 → 千」顺序排列,后续从高位到低位遍历:


const rules = [
  { ones: 'I', fives: 'V', tens: 'IX', four: 'IV', divisor: 1 },    // 个位
  { ones: 'X', fives: 'L', tens: 'XC', four: 'XL', divisor: 10 },   // 十位
  { ones: 'C', fives: 'D', tens: 'CM', four: 'CD', divisor: 100 },  // 百位
  { ones: 'M', fives: '', tens: '', four: '', divisor: 1000 },      // 千位
];

说明:千位无 5 倍(5000)、9 倍(9000)、4 倍(4000)符号,故对应字段设为空,适配范围限制。

步骤 2:遍历规则表复用逻辑

从千位到个位遍历规则表,计算当前位数值后,复用同一套转换逻辑:


// 从高位(千位)到低位(个位)遍历
for (let i = rules.length - 1; i >= 0; i--) {
  const { ones, fives, tens, four, divisor } = rules[i];
  const digit = Math.floor((num % (divisor * 10)) / divisor); // 计算当前位数值
  if (digit === 0) continue; // 位值为 0 跳过,无需拼接符号

  let d = digit;
  while (d > 0) {
    if (d === 9) { res += tens; d -= 9; }
    else if (d === 4) { res += four; d -= 4; }
    else if (d >= 5) { res += fives; d -= 5; }
    else { res += ones; d--; }
  }
}

2. 核心优化点

  • 消除冗余:将 3 段重复逻辑合并为 1 段,修改符号仅需调整规则表,维护成本大幅降低。

  • 适配范围:千位规则字段为空,自然跳过 4/5/9 的判断,符合 1~3999 的输入限制。

  • 逻辑统一:无论哪一位,都遵循「特殊减法优先,加法补充」的规则,一致性更强。

四、关键易错点解析

1. 数据类型陷阱

基础版中若用 toFixed(0) 替代 Math.floor,会返回字符串类型的数值,导致后续比较(如 i === 9)失效。务必用 Math.floor 确保数值类型正确。

2. 千位无需处理特殊情况

因输入最大为 3999,千位数值仅为 0~3,永远不会触发 d===4、d===9、d≥5 的判断,规则表中千位对应字段设为空即可,无需额外逻辑。

3. 高位到低位转换顺序

罗马数字需从高位到低位拼接,因此规则表遍历需从千位(rules.length - 1)开始,若顺序颠倒会导致结果错乱(如 1994 变成 IVXCM 等错误形式)。

五、测试用例验证

针对核心场景测试两种实现,确保结果正确:


// 基础测试
console.log(intToRoman(3));      // III(纯加法)
console.log(intToRoman(4));      // IV(减法形式)
console.log(intToRoman(9));      // IX(减法形式)
console.log(intToRoman(58));     // LVIII(50+5+3)
// 复杂测试
console.log(intToRoman(1994));   // MCMXCIV(1000+900+90+4)
console.log(intToRoman(3999));   // MMMCMXCIX(3000+900+90+9)

两种实现均能通过以上测试,输出符合预期的罗马数字。

六、总结与选择建议

实现对比

实现方式优点缺点适用场景
逐位拆解法逻辑直观、调试简单代码冗余、扩展性差新手入门、快速验证逻辑
规则复用版代码简洁、可维护性强需理解规则抽象逻辑实际开发、算法优化场景

核心思路提炼

罗马数字转换的本质是「按位匹配规则」,优化的关键在于识别重复逻辑并抽象封装。规则复用版通过将「符号与位权」绑定,实现了逻辑的统一,这也是算法优化中「抽象复用」思想的典型应用。

无论是哪种实现,都需紧扣罗马数字的三条核心规则,尤其注意特殊减法和符号累加限制,才能确保转换结果准确无误。