题目描述#
罗马数字包含以下七种字符:I、V、X、L、C、D 和 M。
| 字符 | 数值 |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
例如:
II表示 2XII表示 12XXVII表示 27
通常情况下,罗马数字中小的数字写在大的数字右边,表示相加。但也存在特殊情况:
I可以放在V和X左边,表示 4 和 9X可以放在L和C左边,表示 40 和 90C可以放在D和M左边,表示 400 和 900
给定一个罗马数字字符串,将其转换成对应的整数。
知识边界#
这道题的核心是字符到数值的映射以及相邻字符大小关系的判断。
1. 哈希表映射#
题目中每个罗马字符都对应一个固定整数,因此很适合用哈希表存储映射关系。
Go 中可以使用 map[byte]int:
var Roman = map[byte]int{
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000,
}这里使用 byte 而不是 string,因为字符串中的每个罗马字符都是单个 ASCII 字符,转成字节后访问更直接,效率也更高。
2. 字符串转字节切片#
Go 中字符串可以转成 []byte:
s := []byte(ss)这样就可以通过下标访问每一个字符:
x := s[i]
y := s[i+1]3. 贪心判断相邻关系#
对于当前字符 s[i]:
- 如果它小于右边字符
s[i+1],说明它属于减法组合,应当减去 - 否则它应当加上
例如 IV:
I < V,所以先减去 1- 最后再加上 5
- 结果为 4
4. Go 中的切片遍历#
代码里使用了:
for i, _ := range s[:len(s)-1]含义是遍历到倒数第二个字符,因为循环体内会访问 s[i+1],如果遍历到最后一个字符就会越界。
这是一种很常见的写法,用来处理“当前元素和下一个元素比较”的问题。
5. 常见错误点#
越界访问#
如果直接遍历整个字符串,再访问 s[i+1],最后一次会发生下标越界。
最后一个字符漏算#
因为循环只遍历到倒数第二个字符,所以最后一个字符需要单独加上:
return ans + Roman[s[len(s)-1]]使用 string 逐个比较#
虽然也能做,但本题字符都是单字节,使用 byte 更自然,代码更简洁。
思路#
把罗马数字字符串转成字节切片后,从左到右遍历,比较当前字符和下一个字符的大小关系。
如果当前值小于后一个值,说明当前字符是减法部分,应该从答案中减去;否则说明当前字符是正常加法部分,应该加到答案中。最后循环没有处理最后一个字符,所以再单独补上最后一个字符的值。
思路一#
先建立罗马字符到整数的映射表,然后遍历字符串中除最后一个字符外的所有位置。
设当前字符为 x,下一个字符为 y:
- 若
Roman[x] < Roman[y],则ans -= Roman[x] - 否则
ans += Roman[x]
最后返回:
ans + Roman[s[len(s)-1]]为什么这样做是正确的:
对于罗马数字,只有当一个较小数字出现在较大数字左边时,它才表示减法。题目也明确说明了这种规则只会出现在合法的六种情况中。因此,只需要比较当前字符和后一个字符:
- 小于后一个字符,就应减去当前值
- 否则就应加上当前值
这样每个字符都会被正确处理一次,最后一个字符没有右邻居,必然直接加上即可。
时间复杂度为 O(n),其中 n 是字符串长度,因为只遍历一遍字符串。
空间复杂度为 O(1)。映射表大小固定,额外变量数量也固定。虽然把字符串转成了字节切片,但这一部分按常见题解写法处理,整体辅助开销与输入长度线性相关;若只看除输入外的算法额外结构,核心思路仍然是常数级。
代码#
var Roman = map[byte]int{
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000,
}
func romanToInt(ss string) int {
ans := 0
s := []byte(ss)
for i, _ := range s[:len(s)-1] {
x, y := s[i], s[i+1]
if Roman[x] < Roman[y] {
ans -= Roman[x]
} else {
ans += Roman[x]
}
}
return ans + Roman[s[len(s)-1]]
}