LeetCode 13. 罗马数字转整数(多解法详解 + Java/Python 实现)
题目链接
题目描述
罗马数字包含以下七种字符: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。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4。同样地,数字 9 表示为 IX。
这个特殊的规则只适用于以下六种情况:
I可以放在V(5) 和X(10) 的左边,来表示 4 和 9。X可以放在L(50) 和C(100) 的左边,来表示 40 和 90。C可以放在D(500) 和M(1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。
示例
示例 1
text
输入: s = "III"
输出: 3
示例 2
text
输入: s = "IV"
输出: 4
示例 3
text
输入: s = "IX"
输出: 9
示例 4
text
输入: s = "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.
示例 5
text
输入: s = "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.
解题思路
这道题的核心是理解罗马数字的规则,特别是 特殊情况 的处理。
核心思想
罗马数字通常是从大到小排列的,但有时小的数字会出现在大的数字左边,表示减法。
关键判断 :遍历字符串时,如果当前字符代表的数值 小于 下一个字符代表的数值,说明这是一个特殊情况,当前数值应该被 减去 ;否则,当前数值应该被 加上。
算法步骤
- 建立字符到数值的映射。
- 遍历字符串,对于每个字符:
- 获取当前字符的数值。
- 如果当前字符不是最后一个,且当前数值小于下一个字符的数值,则减去当前数值。
- 否则,加上当前数值。
- 返回总和。
举例说明
以 s = "MCMXCIV" 为例:
| 索引 | 字符 | 当前值 | 下一个值 | 判断 | 操作 | 总和 |
|---|---|---|---|---|---|---|
| 0 | M | 1000 | 100 | 1000 > 100 | +1000 | 1000 |
| 1 | C | 100 | 1000 | 100 < 1000 | -100 | 900 |
| 2 | M | 1000 | 10 | 1000 > 10 | +1000 | 1900 |
| 3 | X | 10 | 100 | 10 < 100 | -10 | 1890 |
| 4 | C | 100 | 1 | 100 > 1 | +100 | 1990 |
| 5 | I | 1 | 5 | 1 < 5 | -1 | 1989 |
| 6 | V | 5 | - | 最后一个 | +5 | 1994 |
最终结果为 1994。
代码实现
Java 版本
版本 1:HashMap 字典版
java
import java.util.HashMap;
import java.util.Map;
class Solution {
public int romanToInt(String s) {
Map<Character, Integer> map = new HashMap<>();
map.put('I', 1);
map.put('V', 5);
map.put('X', 10);
map.put('L', 50);
map.put('C', 100);
map.put('D', 500);
map.put('M', 1000);
int sum = 0;
for (int i = 0; i < s.length(); i++) {
int cur = map.get(s.charAt(i));
if (i < s.length() - 1 && cur < map.get(s.charAt(i + 1))) {
sum -= cur;
} else {
sum += cur;
}
}
return sum;
}
}
版本 2:ASCII 数组查表版(刷题首选)
java
class Solution {
public int romanToInt(String s) {
int[] arr = new int[256];
arr['I'] = 1;
arr['V'] = 5;
arr['X'] = 10;
arr['L'] = 50;
arr['C'] = 100;
arr['D'] = 500;
arr['M'] = 1000;
int sum = 0;
for (int i = 0; i < s.length(); i++) {
int cur = arr[s.charAt(i)];
if (i < s.length() - 1 && cur < arr[s.charAt(i + 1)]) {
sum -= cur;
} else {
sum += cur;
}
}
return sum;
}
}
版本 3:Switch-case 倒序遍历版
java
class Solution {
public int romanToInt(String s) {
int sum = 0;
int prev = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int cur = getVal(s.charAt(i));
if (cur >= prev) {
sum += cur;
} else {
sum -= cur;
}
prev = cur;
}
return sum;
}
private int getVal(char c) {
switch (c) {
case 'I': return 1;
case 'V': return 5;
case 'X': return 10;
case 'L': return 50;
case 'C': return 100;
case 'D': return 500;
case 'M': return 1000;
default: return 0;
}
}
}
Python 版本
版本 1:字典 dict 版本(最标准)
python
class Solution(object):
def romanToInt(self, s):
"""
:type s: str
:rtype: int
"""
roman = {
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000
}
total = 0
n = len(s)
for i in range(n):
cur = roman[s[i]]
if i < n - 1 and cur < roman[s[i+1]]:
total -= cur
else:
total += cur
return total
版本 2:倒序遍历版本
python
class Solution(object):
def romanToInt(self, s):
"""
:type s: str
:rtype: int
"""
roman = {
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000
}
total = 0
prev = 0
for char in reversed(s):
cur = roman[char]
if cur >= prev:
total += cur
else:
total -= cur
prev = cur
return total
代码说明
正向遍历(版本 1 和 2)
- 建立映射 :使用
HashMap或数组存储字符到数值的映射。 - 遍历字符串:从左到右遍历。
- 判断特殊情况 :
- 如果当前字符的数值小于下一个字符的数值,说明是特殊情况(如
IV、IX),当前数值应该被减去。 - 否则,加上当前数值。
- 如果当前字符的数值小于下一个字符的数值,说明是特殊情况(如
- 边界处理:最后一个字符没有下一个字符,直接加上。
倒序遍历(版本 3)
- 从右到左遍历:从字符串末尾开始。
- 记录前一个值 :
prev保存已经处理过的字符的最大数值。 - 判断 :
- 如果当前值大于等于
prev,加上当前值。 - 如果当前值小于
prev,减去当前值(特殊情况)。
- 如果当前值大于等于
- 倒序遍历的优点 :不需要判断
i+1边界,逻辑更简洁。
复杂度分析
所有版本的时间复杂度和空间复杂度都相同:
- 时间复杂度 :
O(n),其中n是字符串的长度,只需要遍历一次。 - 空间复杂度 :
O(1),使用固定大小的映射(HashMap或数组),不随输入规模变化。
版本对比
| 版本 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HashMap | 逻辑清晰,易于理解 | 哈希开销,速度较慢 | 学习理解 |
| ASCII 数组 | 代码简短,速度快 | 需要理解 ASCII 码 | 日常刷题首选 |
| Switch 正向 | 无额外内存,性能高 | 代码较长 | 性能要求高 |
| Switch 倒序 | 不需要判断边界 | 逻辑需要转换 | 拓展思路 |
常见问题解答
Q1:为什么只有六种特殊情况?
因为罗马数字的规则中,只有 I、X、C 可以作为减数,且只能放在特定的大数前面。其他组合不符合规则。
Q2:倒序遍历的原理是什么?
倒序遍历时,我们维护 prev 变量记录已经处理过的字符的最大数值。如果当前字符的数值小于 prev,说明当前字符是作为减数出现的(特殊情况),应该减去;否则加上。
Q3:为什么 ASCII 数组的大小是 256?
因为 ASCII 字符的编码范围是 0-255,使用 256 大小的数组可以覆盖所有可能的字符。实际上,罗马数字字符的 ASCII 码都小于 100,但 256 是一个安全的默认值。
总结
这道题是字符串处理的经典题目,核心要点:
- 理解罗马数字规则:小的数字在大的数字左边表示减法。
- 正向遍历:判断当前值和下一个值的大小关系。
- 倒序遍历 :维护
prev变量,避免边界判断。 - 多种实现方式 :
HashMap、数组、switch-case各有优劣。
关键点回顾:
- 建立字符到数值的映射
- 正向遍历:当前值 < 下一个值则减去,否则加上
- 倒序遍历:当前值 <
prev则减去,否则加上 - 时间复杂度
O(n),空间复杂度O(1)
这道题虽然简单,但很好地考察了对字符串处理和映射的理解。掌握多种实现方式有助于应对不同的面试场景。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注,我会持续更新更多 LeetCode 题解和算法干货!