Appearance
实战:罗马数字转整数
罗马数字是古罗马人发明的一种记数方式,至今仍在特定场景中使用——钟表刻度、书籍章节编号、电影版权年份等。将罗马数字转换为阿拉伯数字,看似是字符串处理问题,实则考验的是模式识别能力。
首先要问一个问题:罗马数字有什么特殊规则?
题目描述
LeetCode 13. 罗马数字转整数
罗马数字包含以下七种字符: I, V, X, L, C, D 和 M。
字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如,罗马数字 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;
}这种方法更直观——把特殊组合当作独立单位处理,避免了减法逻辑。
权衡:代码更直观,但映射表更大,且每次都要尝试切片操作。对于这道题,两种方法性能差异不大,选择你觉得更清晰的即可。
常见错误
- 遍历方向混乱:从左向右需要比较"当前与下一个",从右向左比较"当前与上一个",不要搞混
- 忘记更新 prev:每轮循环结束必须更新
prev = curr - 边界条件:单字符字符串,直接返回映射值即可
本章小结
罗马数字转整数的核心在于识别减法规则:小值在大值左边时减。
从右向左遍历是一个技巧——它将"前后比较"简化为"当前与已知值比较",代码更简洁。这种调整遍历方向来简化逻辑的思路,在很多问题中都适用。
相关题目
- 12. 整数转罗马数字:逆向问题,从整数转成罗马数字
- 8. 字符串转换整数 (atoi):另一种字符串解析问题
- 7. 整数反转:数字处理的边界情况
下一章我们看逆问题:如何将整数转换为罗马数字?