「LeetCode」13.罗马数字转整数

280 阅读3分钟

题目描述🌍

罗马数字包含以下七种字符: IVXLCDM

字符          数值
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 <= 15
  • s 仅含字符 ('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;
    }
}

时间复杂度:O(n)O(n)

空间复杂度:O(1)O(1)

降序排列遍历法🌄

解题思路

通常情况下,罗马数字中小的数字在大的数字的右边。那么,我们只需要将字符串与其对应的值同罗马数字一样降序排列,便可以不借用 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;
    }
};

时间复杂度:O(n)O(n)

空间复杂度:O(1)O(1)

相对位置加减法🌘

解题思路

通常情况下,罗马数字中小的数字在大的数字的右边。若输入的字符串满足该情况,那么可以将每个字符视作一个单独的值,累加每个字符对应的数值即可。

例如: XXVIIXXVII 可视作 X+X+V+I+I=10+10+5+1+1=27X+X+V+I+I=10+10+5+1+1=27

若存在小的数字在大的数字的左边的情况,根据规则需要减去小的数字。对于这种情况,我们也可以将每个字符视作一个单独的值,若一个数字右侧的数字比它大,则将该数字的符号取反。

例如: XIVXIV 可视作 XI+V=101+5X-I+V=10-1+5

代码

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;
    }
};

时间复杂度:O(n)O(n)

空间复杂度:O(1)O(1)

逐一提前判断法/暴力枚举法😡

解题思路

将罗马数字转换成字符数组 char[],然后判断当前遍历的字符与下一个字符的关系(只有 IXC 需要提前判断),若合并为特例则进行对应的增值,否则直接累加该字符对应的值。

这种方法又称暴力枚举法,其实是最简单最粗暴、效率又是最高的方法。

代码

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;
    }
};

时间复杂度:O(n)O(n)

空间复杂度:O(1)O(1)

知识点🌓

在 "相对位置加减法" 中存在这样一段代码:

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. 推而广之

这种写法,推而广之,在初始化 ArrayListSet 的时候都可以这么玩,比如你还可以这么玩:

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. 文艺写法的潜在问题

文章开头提到的文艺写法的好处很明显就是一目了然

这里来罗列下此种方法的坏处,如果这个对象要串行化,可能会导致串行化失败。

  1. 此种方式是匿名内部类的声明方式,所以引用中持有着外部类的引用。所以当串行化这个集合时外部类也会被不知不觉的串行化,当外部类没有实现serialize接口时,就会报错。
  2. 上例中,其实是声明了一个继承自 HashMap 的子类。然而有些串行化方法,例如要通过 Gson 串行化为 json,或者要串行化为 xml 时,类库中提供的方式,是无法串行化 HashSet 或者 HashMap 的子类的,从而导致串行化失败。

解决办法:重新初始化为一个 HashMap 对象:

new HashMap(map);

这样就可以正常初始化了。

4. 执行效率问题

当一种新的工具或者写法出现时,猿们都会来一句:性能怎么样?

关于这个两种写法我这边笔记本上测试文艺写法、普通写法分别创建 10,000,00010,000,000 个 Map 的结果是 1217121710641064,相差 13%13\%

5. 更多

详见博客:blog.csdn.net/luman1991/a…

最后🌅

该篇文章为 「LeetCode」 系列的 No.5 篇,在这个系列文章中:

  • 尽量给出多种解题思路
  • 提供题解的多语言代码实现
  • 记录该题涉及的知识点

👨‍💻争取做到 LeetCode 每日 1 题,所有的题解/代码均收录于 「LeetCode」 仓库,欢迎随时加入我们的刷题小分队!