罗马数字作为古老的计数体系,其转换规则围绕「加法优先、特殊减法补充」设计,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)
两种实现均能通过以上测试,输出符合预期的罗马数字。
六、总结与选择建议
实现对比
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 逐位拆解法 | 逻辑直观、调试简单 | 代码冗余、扩展性差 | 新手入门、快速验证逻辑 |
| 规则复用版 | 代码简洁、可维护性强 | 需理解规则抽象逻辑 | 实际开发、算法优化场景 |
核心思路提炼
罗马数字转换的本质是「按位匹配规则」,优化的关键在于识别重复逻辑并抽象封装。规则复用版通过将「符号与位权」绑定,实现了逻辑的统一,这也是算法优化中「抽象复用」思想的典型应用。
无论是哪种实现,都需紧扣罗马数字的三条核心规则,尤其注意特殊减法和符号累加限制,才能确保转换结果准确无误。