题目描述🌍
罗马数字包含以下七种字符: 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 到 3999 的范围内。
示例 1:
输入: "III"
输出: 3
示例 2:
输入: "IV"
输出: 4
示例 3:
输入: "IX"
输出: 9
示例 4:
输入: "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.
示例 5:
输入: "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.
提示:
1 <= s.length <= 15s仅含字符('I', 'V', 'X', 'L', 'C', 'D', 'M')- 题目数据保证
s是一个有效的罗马数字,且表示整数在范围[1, 3999]内 - 题目所给测试用例皆符合罗马数字书写规则,不会出现跨位等情况。
- IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。
- 关于罗马数字的详尽书写规则,可以参考 罗马数字 - Mathematics 。
字符替换法🙄
解题思路
我们很容易想到将罗马数字转换为 char[] 型然后逐一判断,不过这样必须得考虑前后字符间的关系再进行加减运算。如果非要单个字符逐一判断呢?那就把所有特殊的 2 字符转换为其他不相冲突的 1 字符,这样每一个字符都是独立且唯一的存在了,接下来便可进行逐一字符判断。
代码
Java
class Solution {
public int romanToInt(String s) {
// 将所有2字符替换成1字符
s = s.replace("IV", "a");
s = s.replace("IX", "b");
s = s.replace("XL", "c");
s = s.replace("XC", "d");
s = s.replace("CD", "e");
s = s.replace("CM", "f");
Map<Character, Integer> hashMap = new HashMap<>();
hashMap.put('I', 1);
hashMap.put('V', 5);
hashMap.put('X', 10);
hashMap.put('L', 50);
hashMap.put('C', 100);
hashMap.put('D', 500);
hashMap.put('M', 1000);
hashMap.put('a', 4);
hashMap.put('b', 9);
hashMap.put('c', 40);
hashMap.put('d', 90);
hashMap.put('e', 400);
hashMap.put('f', 900);
char[] romans = s.toCharArray();
int result = 0;
// 逐一判断即可
for (int i = 0; i < romans.length; i++) {
result += hashMap.get(romans[i]);
}
return result;
}
}
时间复杂度:
空间复杂度:
降序排列遍历法🌄
解题思路
通常情况下,罗马数字中小的数字在大的数字的右边。那么,我们只需要将字符串与其对应的值同罗马数字一样降序排列,便可以不借用 HashMap 直接对由罗马数字转换而来的字符串进行顺序遍历。
注意:每种字符串不一定只出现一次!
代码
Java
class Solution {
int[] val = new int[]{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] key = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
public int romanToInt(String s) {
int n = s.length();
int pointer = 0;
int ans = 0;
for (int i = 0; i < val.length && pointer < n; i++) {
// 当前遍历到的字符串的长度
int length = key[i].length();
// substring: [闭区间,开区间)
// while: 每个字符串不一定只出现一次,但一定是顺序出现!
while (pointer + length <= n && s.substring(pointer, pointer + length).equals(key[i])) {
ans += val[i];
pointer += length;
}
}
return ans;
}
}
C++
class Solution {
public:
int val[13] = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
string key[13] = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
int romanToInt(string s) {
int n = s.length();
int pointer = 0;
int ans = 0;
for (int i = 0; i < 13; ++i) {
int length = key[i].length();
while (pointer + length <= n && s.substr(pointer, length) == key[i]) {
ans += val[i];
pointer += length;
}
}
return ans;
}
};
时间复杂度:
空间复杂度:
相对位置加减法🌘
解题思路
通常情况下,罗马数字中小的数字在大的数字的右边。若输入的字符串满足该情况,那么可以将每个字符视作一个单独的值,累加每个字符对应的数值即可。
例如: 可视作 。
若存在小的数字在大的数字的左边的情况,根据规则需要减去小的数字。对于这种情况,我们也可以将每个字符视作一个单独的值,若一个数字右侧的数字比它大,则将该数字的符号取反。
例如: 可视作 。
代码
Java
class Solution {
public int romanToInt(String s) {
Map<Character, Integer> map = new HashMap<>() {{
put('I', 1);
put('V', 5);
put('X', 10);
put('L', 50);
put('C', 100);
put('D', 500);
put('M', 1000);
}};
int result = 0;
int n = s.length();
for (int i = 0; i < n; i++) {
int pre = map.get(s.charAt(i));
// i < n-1: 防止溢出
// pre 与 map.get(s.charAr(i+1)): 前后指针的关系
if (i < n - 1 && pre < map.get(s.charAt(i + 1))) {
result -= pre;
} else {
result += pre;
}
}
return result;
}
}
C++
class Solution {
public:
int romanToInt(string s) {
unordered_map<char, int> roman{
{'I', 1},
{'V', 5},
{'X', 10},
{'L', 50},
{'C', 100},
{'D', 500},
{'M', 1000},
};
int result = 0;
int n = s.length();
for (int i = 0; i < n; ++i) {
int pre = roman[s[i]];
if (i < n - 1 && pre < roman[s[i + 1]]) {
result -= pre;
} else {
result += pre;
}
}
return result;
}
};
时间复杂度:
空间复杂度:
逐一提前判断法/暴力枚举法😡
解题思路
将罗马数字转换成字符数组 char[],然后判断当前遍历的字符与下一个字符的关系(只有 I、X、C 需要提前判断),若合并为特例则进行对应的增值,否则直接累加该字符对应的值。
这种方法又称暴力枚举法,其实是最简单最粗暴、效率又是最高的方法。
代码
Java
class Solution {
public int romanToInt(String s) {
char[] romans = s.toCharArray();
int sum = 0;
for (int i = 0; i < romans.length; i++) {
switch (romans[i]) {
case 'V':
sum += 5;
break;
case 'L':
sum += 50;
break;
case 'D':
sum += 500;
break;
case 'M':
sum += 1000;
break;
case 'I':
if (i + 1 < romans.length && romans[i + 1] == 'X') {
sum += 9;
i++;
} else if (i + 1 < romans.length && romans[i + 1] == 'V') {
sum += 4;
i++;
} else {
sum += 1;
}
break;
case 'X':
if (i + 1 < romans.length && romans[i + 1] == 'L') {
sum += 40;
i++;
} else if (i + 1 < romans.length && romans[i + 1] == 'C') {
sum += 90;
i++;
} else {
sum += 10;
}
break;
case 'C':
if (i + 1 < romans.length && romans[i + 1] == 'D') {
sum += 400;
i++;
} else if (i + 1 < romans.length && romans[i + 1] == 'M') {
sum += 900;
i++;
} else {
sum += 100;
}
break;
default:
throw new RuntimeException();
}
}
return sum;
}
}
C++
class Solution {
public:
int romanToInt(string s) {
int ret = 0;
for (int i = 0; i < s.length(); i++) {
switch (s[i]) {
case 'V':
ret += 5;
break;
case 'L':
ret += 50;
break;
case 'D':
ret += 500;
break;
case 'M':
ret += 1000;
break;
case 'I':
if (i + 1 < s.length() && s[i + 1] == 'X') {
ret += 9;
i++;
} else if (i + 1 < s.length() && s[i + 1] == 'V') {
ret += 4;
i++;
} else {
ret += 1;
}
break;
case 'X':
if (i + 1 < s.length() && s[i + 1] == 'L') {
ret += 40;
i++;
} else if (i + 1 < s.length() && s[i + 1] == 'C') {
ret += 90;
i++;
} else {
ret += 10;
}
break;
case 'C':
if (i + 1 < s.length() && s[i + 1] == 'D') {
ret += 400;
i++;
} else if (i + 1 < s.length() && s[i + 1] == 'M') {
ret += 900;
i++;
} else {
ret += 100;
}
break;
}
}
return ret;
}
};
时间复杂度:
空间复杂度:
知识点🌓
在 "相对位置加减法" 中存在这样一段代码:
Map<Character, Integer> map = new HashMap<>() {{
put('I', 1);
put('V', 5);
...
}};
嗯?这是什么非主流写法?!以下我们来详细探讨下关于这个芝士点的相关内容!
1. HashMap 初始化文艺写法
HashMap 是一种常用的数据结构,一般用来做数据字典或者 Hash 查找的容器;普通青年一般会这么初始化:
HashMap<String, String> map = new HashMap<>();
map.put("Name", "w");
map.put("QQ", "5771*****");
看完这段代码,很多人都会觉得这么写太啰嗦了,对此,文艺青年一般这么来了:
HashMap<String, String> map = new HashMap<String, String>() {
{
put("Name", "w");
put("QQ", "5771*****");
}
};
这里的双括号到底什么意思,什么用法呢?哈哈,其实很简单,看看下面的代码你就知道了。
public class Test {
/*
private static HashMap<String, String> map = new HashMap<String, String>() {
{
put("Name", "w");
put("QQ", "5771*****");
}
};
*/
public Test() {
System.out.println("Constructor called:构造器被调用");
}
static {
System.out.println("Static block called:静态块被调用");
}
{
System.out.println("Instance initializer called:实例初始化块被调用");
}
public static void main(String[] args) {
new Test();
System.out.println("=======================");
new Test();
}
}
输出:
Static block called:静态块被调用
Instance initializer called:实例初始化被调用
Constructor called:构造器被调用
=======================
Instance initializer called:实例初始化被调用
Constructor called:构造器被调用
- 第一层括弧实际是定义了一个匿名内部类 (Anonymous Inner Class)
- 第二层括弧实际上是一个实例初始化块 (instance initializer block),这个块在内部匿名类构造时被执行;这个块之所以被叫做 “实例初始化块” 是因为它们被定义在了一个类的实例范围内
上面代码如果是写在 Test 类中,编译后你会看到会生成 Test$1.class 文件,反编译该文件内容:
D:eclipse_indigoworkspace_homeCDHJobsbinpvuv>jad -p Test$1.class
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Test.java
Test$1.class:
class Test$1 extends HashMap // 创建了一个 HashMap 的子类
{
// 第二个 {} 中的代码放到了构造方法中去了
Test$1() {
put("Name", "w");
put("QQ", "5771*****");
}
}
2. 推而广之
这种写法,推而广之,在初始化 ArrayList、Set 的时候都可以这么玩,比如你还可以这么玩:
List<String> names = new ArrayList<String>() {{
for (int i = 0; i < 10; i++) {
add("A" + i);
}
}};
System.out.println(names.toString()); // [A0, A1, A2, A3, A4, A5, A6, A7, A8, A9]
3. 文艺写法的潜在问题
文章开头提到的文艺写法的好处很明显就是一目了然。
这里来罗列下此种方法的坏处,如果这个对象要串行化,可能会导致串行化失败。
- 此种方式是匿名内部类的声明方式,所以引用中持有着外部类的引用。所以当串行化这个集合时外部类也会被不知不觉的串行化,当外部类没有实现serialize接口时,就会报错。
- 上例中,其实是声明了一个继承自
HashMap的子类。然而有些串行化方法,例如要通过Gson串行化为json,或者要串行化为xml时,类库中提供的方式,是无法串行化HashSet或者HashMap的子类的,从而导致串行化失败。
解决办法:重新初始化为一个 HashMap 对象:
new HashMap(map);
这样就可以正常初始化了。
4. 执行效率问题
当一种新的工具或者写法出现时,猿们都会来一句:性能怎么样?
关于这个两种写法我这边笔记本上测试文艺写法、普通写法分别创建 个 Map 的结果是 、,相差 。
5. 更多
详见博客:blog.csdn.net/luman1991/a…
最后🌅
该篇文章为 「LeetCode」 系列的 No.5 篇,在这个系列文章中:
- 尽量给出多种解题思路
- 提供题解的多语言代码实现
- 记录该题涉及的知识点
👨💻争取做到 LeetCode 每日 1 题,所有的题解/代码均收录于 「LeetCode」 仓库,欢迎随时加入我们的刷题小分队!