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

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

一、题目核心规则梳理

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

1. 基础符号映射

符号 符号
I 1 L 50
V 5 C 100
X 10 D 500
M 1000 - -

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 确保结果为整数(避免浮点数干扰):

typescript 复制代码
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 倍符号、位权」封装为对象,按「个 → 千」顺序排列,后续从高位到低位遍历:

typescript 复制代码
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:遍历规则表复用逻辑

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

typescript 复制代码
// 从高位(千位)到低位(个位)遍历
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 等错误形式)。

五、测试用例验证

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

typescript 复制代码
// 基础测试
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)

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

六、总结与选择建议

实现对比

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

核心思路提炼

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

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

相关推荐
努力也学不会java1 小时前
【缓存算法】一篇文章带你彻底搞懂面试高频题LRU/LFU
java·数据结构·人工智能·算法·缓存·面试
jzlhll1231 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin
旖-旎1 小时前
二分查找(x的平方根)(4)
c++·算法·二分查找·力扣·双指针
ECT-OS-JiuHuaShan1 小时前
朱梁万有递归元定理,重构《易经》
算法·重构
蓝冰凌2 小时前
Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用
前端·javascript·vue.js
奔跑的呱呱牛2 小时前
generate-route-vue基于文件系统的 Vue Router 动态路由生成工具
前端·javascript·vue.js
智者知已应修善业2 小时前
【51单片机独立按键控制数码管移动反向,2片74CH573/74CH273段和位,按键按下保持原状态】2023-3-25
经验分享·笔记·单片机·嵌入式硬件·算法·51单片机
柳杉2 小时前
从动漫水面到赛博飞船:这位开发者的Three.js作品太惊艳了
前端·javascript·数据可视化
khddvbe2 小时前
C++并发编程中的死锁避免
开发语言·c++·算法