如何把罗马数字转换成整数?(LeetCode 13)
简化版
把罗马数字字符串转成整数。罗马数字用 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)。核心规则:从左到右扫描,如果当前字符代表的值 < 它右边字符的值,就减去它,否则加上它。因为「小的在大的左边」意味着这是减法组合。
详细版
int romanToInt(String s) {
Map<Character, Integer> val = Map.of(
'I', 1, 'V', 5, 'X', 10, 'L', 50,
'C', 100, 'D', 500, 'M', 1000);
int total = 0;
for (int i = 0; i < s.length(); i++) {
int cur = val.get(s.charAt(i));
// 若当前值 < 右边值,说明是减法组合(如 IV),减去当前值
if (i + 1 < s.length() && cur < val.get(s.charAt(i + 1))) {
total -= cur;
} else {
total += cur;
}
}
return total;
}
- 映射表:7 个罗马字符到数值。
- 判断加还是减:当前字符值
<右邻字符值 → 减(减法组合的前一位),否则加。 - 减法组合:IV、IX、XL、XC、CD、CM 共 6 种,都是「小值在大值左边」。
- 复杂度:O(n) 时间、O(1) 空间。
完整版教学
一、罗马数字规则速览
罗马数字用 7 个字母表示数值:
| 字符 | I | V | X | L | C | D | M |
|---|---|---|---|---|---|---|---|
| 值 | 1 | 5 | 10 | 50 | 100 | 500 | 1000 |
基本规则:一般从大到小排列、数值相加(如 MCMXCIV… 先不管)。VIII = 5+1+1+1 = 8,XXVII = 10+10+5+1+1 = 27。
特殊规则(减法):为避免连写四个相同字符,规定 6 种「小值放在大值左边表示减」的组合:
IV = 4(5-1)、IX = 9(10-1)XL = 40(50-10)、XC = 90(100-10)CD = 400(500-100)、CM = 900(1000-100)
二、核心洞察:小在大前则减
观察减法组合的共性:当一个较小的数值出现在较大数值的左边时,它要被减掉。而正常情况(大在前、小在后)是相加。所以有个统一的判断规则:
从左到右扫描每个字符,若它的值
<右边相邻字符的值,就减去它;否则加上它。
IV:I(1) < V(5) → 减 I,即-1 + 5 = 4。VI:V(5) > I(1) → 加 V,5 + ...,I 是最后一位直接加,5 + 1 = 6。
这个规则一举涵盖了所有加法和 6 种减法情形,不用单独枚举减法对。
三、逐位处理逻辑
遍历字符串每个字符 s[i]:
- 取它的值
cur。 - 看右邻:若
i+1存在且cur < val[s[i+1]](当前比右边小)→total -= cur(它是减法组合的前一位)。 - 否则 →
total += cur。
最后一位没有右邻,一定是加。累加完即结果。
例 MCMXCIV(1994):
- M(1000) ≥ C → +1000
- C(100) < M(1000) → -100
- M(1000) ≥ X → +1000
- X(10) < C(100) → -10
- C(100) ≥ I → +100
- I(1) < V(5) → -1
- V(5) 最后一位 → +5
- 合计
1000-100+1000-10+100-1+5 = 1994。✔
四、另一种等价写法:先全加、遇减法对减两倍
也可以「先把所有字符值全加起来,再对每个减法组合减去 2 倍的小值」。但「比较相邻、小则减」的写法更简洁通用,推荐。用 HashMap 或 switch 存映射都行,字符少,O(1) 空间。
五、反向问题:整数转罗马数字(12)
孪生题 整数转罗马数字(12)用贪心:准备一个从大到小的「数值-符号」对照表(含 1000、900、500、400、100、90、50、40、10、9、5、4、1 及对应符号),从最大的开始,能减就减、拼上对应符号,直到把整数减到 0。两题一起掌握,一个是「罗马→数字」的模拟、一个是「数字→罗马」的贪心。
六、相邻比较为何能统一加减规则
合法罗马数字中,某个较小符号放在更大符号之前时,它就是减法贡献;否则为加法贡献。逐字符看右邻相当于把 IV 计算为 -I+V,把普通降序段逐项相加,不需要先把字符配成 token。
MCMXCIV
M=+1000
C<M -> -100,M=+1000
X<C -> -10,C=+100
I<V -> -1,V=+5
总和 1000-100+1000-10+100-1+5
结果 1994
| 校验维度 | 本题必须保持的结论 |
|---|---|
| 循环/递推不变量 | 扫描到 i 后,total 是已决定符号的前缀贡献;只有当前值与右邻关系决定当前正负。 |
| 边界条件 | 题目通常保证输入是合法罗马数字;若要求校验合法性,仅靠“小于右邻则减”不够。 |
| 复杂度与代价 | 单次线性扫描 O(n),映射表固定 7 项为 O(1) 空间。 |
| 自检方式 | 用最小输入、典型输入和触及边界的输入分别手推,结果应与定义一致。 |
这组推演的作用不是替代证明,而是把抽象规则落到每一步状态变化上。面试写完代码后,应主动指出不变量如何保证不漏解,以及边界为何不会越界或死循环。若手推结果与定义不一致,应先修正状态语义,而不是继续给代码打补丁。
七、面试现场如何验证这道题
数学题不能只凭样例相信公式,必须同时核对定义域、推导条件和定宽整数边界。本题应先复述这条不变量:扫描到 i 后,total 是已决定符号的前缀贡献;只有当前值与右邻关系决定当前正负。只有代码的初始化、更新顺序和终止条件都维持它,样例通过才具有证明力。
- 典型路径从“MCMXCIV”开始手推,最后应得到“结果 1994”。
- 边界复核:题目通常保证输入是合法罗马数字;若要求校验合法性,仅靠“小于右邻则减”不够。
- 代价复核:单次线性扫描 O(n),映射表固定 7 项为 O(1) 空间。
- 用 0、1、最小合法值和最大合法值检查公式的定义域。
- 乘法、取绝对值或取负前先判断是否可能触及
Integer.MIN_VALUE等不对称边界。 - 若算法依赖单调性、抵消或整除关系,要明确题设保证何时成立、何时必须额外验证。
最后要主动构造一个“少写一次循环、改变一个不等号或交换两行更新就会失败”的反例。能说清反例破坏了哪条不变量,比只报出复杂度更能证明真正掌握了算法。
记忆钩子:本题的代码可以压缩,但“扫描到 i 后,total 是已决定符号的前缀贡献;只有当前值与右邻关系决定当前正负。”这条正确性主线不能省。
八、常见误区与追问
- 误区:任意小字符放在大字符前都构成合法减法。 合法表示只允许 IV、IX、XL、XC、CD、CM 等规定组合;本题输入合法才可简化判断。
- 误区:遇到减法组合要同时跳过两个字符。 逐字符正负贡献法无需跳过,前者减、后者正常加即可。
- 误区:最后一个字符也要与右邻比较。 它没有右邻,贡献必为正,直接相加。
- 追问:
III如何计算? 每个 I 都不小于右邻或位于末尾,因此1+1+1=3。 - 追问:整数转罗马为何使用降序表? 每次选择不超过剩余值的最大合法符号,包含减法组合后可贪心构造规范表示。
- 追问:若输入可能是
IL怎么办? 数值扫描会算出 49,但它不是规范写法;需要额外语法规则或回转后比对来验证。
九、加强记忆
罗马数字转整数 = 从左扫描,「小在大前则减,否则加」。7 个字符映射到值(I/V/X/L/C/D/M = 1/5/10/50/100/500/1000)。规则:若当前字符值 < 右邻字符值 → total -= cur(减法组合前一位),否则 total += cur。这一条统一涵盖 6 种减法组合(IV/IX/XL/XC/CD/CM)和所有加法。O(n) O(1)。反向题「整数转罗马」用贪心(大数值符号优先能减就拼)。核心:小的在大的左边,就减掉它。