Skip to content

实战:罗马数字转整数

罗马数字是古罗马人发明的一种记数方式,至今仍在特定场景中使用——钟表刻度、书籍章节编号、电影版权年份等。将罗马数字转换为阿拉伯数字,看似是字符串处理问题,实则考验的是模式识别能力

首先要问一个问题:罗马数字有什么特殊规则?

题目描述

LeetCode 13. 罗马数字转整数

罗马数字包含以下七种字符: I, V, X, L, C, D 和 M。

字符数值
I1
V5
X10
L50
C100
D500
M1000

例如,罗马数字 2 写做 II,即为两个并列的 1。12 写做 XII,即为 X + II。27 写做 XXVII, 即为 XX + V + II。

特殊规则(减法规则)

  • I 可以放在 V (5) 和 X (10) 的左边,表示 4 和 9
  • X 可以放在 L (50) 和 C (100) 的左边,表示 40 和 90
  • C 可以放在 D (500) 和 M (1000) 的左边,表示 400 和 900

示例

输入: s = "III"
输出: 3

输入: s = "LVIII"
输出: 58
解释: L = 50, V = 5, III = 3

输入: s = "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4

规则提炼

现在我要问第二个问题:如何判断应该加还是应该减?

观察特殊规则:IV = 4, IX = 9, XL = 40, XC = 90, CD = 400, CM = 900。

发现规律了吗?

当小值在大值左边时,执行减法。IV 中 I(1) < V(5),所以是 5 - 1 = 4。

这意味着我们遍历时需要同时考虑当前字符下一个字符的大小关系。

但如果从左向右遍历,每次都要判断"当前值是否小于下一个值",稍显麻烦。思考一下,有没有更优雅的方式?

有!从右向左遍历

此时规则变成:当前值 < 上一个值(右边的值),执行减法。

解法:从右向左遍历

javascript
function romanToInt(s) {
    const map = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000
    };
    
    let result = 0;
    let prev = 0;  // 记录右边(上一个)的值
    
    // 从右向左遍历
    for (let i = s.length - 1; i >= 0; i--) {
        const curr = map[s[i]];
        
        if (curr < prev) {
            // 当前值 < 右边的值,减法规则
            result -= curr;
        } else {
            // 否则正常累加
            result += curr;
        }
        
        prev = curr;
    }
    
    return result;
}

为什么从右向左更自然?因为我们用一个变量 prev 记录"右边的值",判断时只需比较 curr < prev,代码更简洁。

执行过程追踪

s = "MCMXCIV" 为例:

从右向左遍历:V → I → C → X → M → C → M

i=6: 'V'=5,    prev=0,    5>=0,     result=5,    prev=5
i=5: 'I'=1,    prev=5,    1<5,      result=4,    prev=1    // 减法!
i=4: 'C'=100,  prev=1,    100>=1,   result=104,  prev=100
i=3: 'X'=10,   prev=100,  10<100,   result=94,   prev=10   // 减法!
i=2: 'M'=1000, prev=10,   1000>=10, result=1094, prev=1000
i=1: 'C'=100,  prev=1000, 100<1000, result=994,  prev=100  // 减法!
i=0: 'M'=1000, prev=100,  1000>=100,result=1994

返回 1994

有没有很巧妙?通过方向的选择,把复杂的前后判断简化为单纯的大小比较。

复杂度分析

  • 时间复杂度:O(n),遍历一次字符串
  • 空间复杂度:O(1),映射表大小固定

解法二:预处理特殊组合

另一个思路是把六个特殊组合也加入映射表,优先匹配两字符的组合:

javascript
function romanToInt(s) {
    const map = {
        'I': 1, 'V': 5, 'X': 10, 'L': 50,
        'C': 100, 'D': 500, 'M': 1000,
        // 特殊组合
        'IV': 4, 'IX': 9, 'XL': 40, 'XC': 90,
        'CD': 400, 'CM': 900
    };
    
    let result = 0;
    let i = 0;
    
    while (i < s.length) {
        // 优先尝试匹配两字符
        const twoChar = s.slice(i, i + 2);
        
        if (map[twoChar] !== undefined) {
            result += map[twoChar];
            i += 2;
        } else {
            result += map[s[i]];
            i++;
        }
    }
    
    return result;
}

这种方法更直观——把特殊组合当作独立单位处理,避免了减法逻辑。

权衡:代码更直观,但映射表更大,且每次都要尝试切片操作。对于这道题,两种方法性能差异不大,选择你觉得更清晰的即可。

常见错误

  1. 遍历方向混乱:从左向右需要比较"当前与下一个",从右向左比较"当前与上一个",不要搞混
  2. 忘记更新 prev:每轮循环结束必须更新 prev = curr
  3. 边界条件:单字符字符串,直接返回映射值即可

本章小结

罗马数字转整数的核心在于识别减法规则:小值在大值左边时减。

从右向左遍历是一个技巧——它将"前后比较"简化为"当前与已知值比较",代码更简洁。这种调整遍历方向来简化逻辑的思路,在很多问题中都适用。

相关题目

  • 12. 整数转罗马数字:逆向问题,从整数转成罗马数字
  • 8. 字符串转换整数 (atoi):另一种字符串解析问题
  • 7. 整数反转:数字处理的边界情况

下一章我们看逆问题:如何将整数转换为罗马数字?

实战:罗马数字转整数 has loaded