LeetCode 13. 罗马数字转整数:从规则拆解到代码实现

8 阅读5分钟

在 LeetCode 简单题中,罗马数字转整数是一道经典的“规则理解+逻辑实现”类题目。它不仅考察对罗马数字计数规则的掌握,更能体现代码的简洁性与高效性。本文将从题目规则拆解入手,逐步分析解题思路,最终详解代码实现,并补充优化方向,帮助大家彻底吃透这道题。

一、题目核心规则梳理

罗马数字由 7 个基础字符组成,对应固定数值,这是解题的基础:

罗马字符对应数值
I1
V5
X10
L50
C100
D500
M1000

罗马数字的计数有两个核心规则,直接决定解题逻辑:

  1. 常规规则:小数值字符在大数值字符右侧,数值为两者之和(如 XII = 10 + 1 + 1 = 12,XXVII = 10 + 10 + 5 + 1 + 1 = 27)。

  2. 特殊规则:仅 6 种特例,小数值字符在大数值字符左侧,数值为大数减小数(如 IV = 5 - 1 = 4,IX = 10 - 1 = 9;XL = 50 - 10 = 40,XC = 100 - 10 = 90;CD = 500 - 100 = 400,CM = 1000 - 100 = 900)。

二、解题思路推导

拿到题目后,最直观的想法是“逐个字符处理,结合相邻字符判断加减”。核心逻辑在于:遍历每个字符时,对比当前字符与下一个字符的数值大小

  • 若当前字符数值 < 下一个字符数值:符合特殊规则,需用结果减去当前数值(后续加上下一个数值,等价于“大数减小数”)。

  • 若当前字符数值 ≥ 下一个字符数值:符合常规规则,直接将当前数值加入结果。

这个思路的优势的是“一次遍历即可完成计算”,时间复杂度为 O(n)(n 为罗马数字字符串长度),空间复杂度为 O(1)(仅用有限变量存储结果和映射关系),是最优解之一。

三、代码实现与逐行解析

基于上述思路,给出 TypeScript 代码实现,并逐行拆解逻辑:



function romanToInt(s: string): number {
  // 1. 建立罗马字符与数值的映射表,方便快速查询
  const search: Record<string, number> = {
    'I': 1,
    'V': 5,
    'X': 10,
    'L': 50,
    'C': 100,
    'D': 500,
    'M': 1000
  }
  // 2. 将字符串拆分为字符数组(也可直接通过索引访问,无需拆分,此处为直观起见)
  const sArr: string[] = s.split('');
  // 3. 初始化结果变量
  let res = 0;
  // 4. 遍历字符数组,结合索引和值处理
  for (let [index, value] of sArr.entries()) {
    // 5. 获取下一个字符的数值,若为最后一个字符,下一个数值默认为 0(避免越界)
    const next = search[sArr[index + 1]] || 0;
    // 6. 对比当前值与下一个值,判断加减
    if (search[value] < next) {
      res -= search[value];
    } else {
      res += search[value];
    }
  }
  // 7. 返回最终结果
  return res;
};
      

关键细节说明

  1. 映射表设计:用对象(Record 类型)存储映射关系,查询时间复杂度为 O(1),比数组查询更高效。

  2. 越界处理:当遍历到最后一个字符时,index + 1 超出数组长度,此时 next 设为 0,确保逻辑统一(最后一个字符必然满足“当前值 ≥ 0”,直接加至结果)。

  3. 字符数组拆分:s.split('') 并非必需,可直接通过 s[index] 和 s[index+1] 访问字符,进一步简化代码(下文优化会提及)。

四、代码优化与拓展

优化版本(精简代码,去除数组拆分)

无需将字符串拆分为数组,直接通过索引遍历字符串,减少内存占用:



function romanToInt(s: string): number {
  const map: Record<string, number> = { 'I':1, 'V':5, 'X':10, 'L':50, 'C':100, 'D':500, 'M':1000 };
  let res = 0;
  for (let i = 0; i < s.length; i++) {
    // 直接通过字符串索引访问,无需数组
    if (map[s[i]] < map[s[i+1]]) {
      res -= map[s[i]];
    } else {
      res += map[s[i]];
    }
  }
  return res;
};
      

该版本逻辑与原版本一致,但代码更简洁,空间复杂度进一步优化(无额外字符数组占用)。

其他解题思路(了解即可,效率略低)

除了“逐字符对比法”,还可通过“替换特殊组合”的思路解题:

  1. 将 6 种特殊组合(IV、IX、XL、XC、CD、CM)替换为对应的数值字符串(如 IV 替换为 4,IX 替换为 9)。

  2. 遍历替换后的字符串,累加每个字符对应的数值。

该思路代码可读性较差,且替换操作会增加时间开销,不如“逐字符对比法”高效,仅作为思路拓展。

五、测试用例验证

为确保代码正确性,可通过以下典型测试用例验证:

  • 常规用例:s = "III" → 3;s = "LVIII" → 58(50 + 5 + 3)。

  • 特殊用例:s = "IV" → 4;s = "IX" → 9;s = "MCMXCIV" → 1994(1000 + 900 + 90 + 4)。

  • 边界用例:s = "M" → 1000(单个字符);s = "CMXCIX" → 999(全为特殊组合)。

将上述用例代入代码,均能得到正确结果,说明逻辑无漏洞。

六、总结

罗马数字转整数的核心是吃透特殊规则,通过相邻字符对比简化逻辑。本文给出的最优解法具备 O(n) 时间复杂度和 O(1) 空间复杂度,代码简洁且易维护。

这道题的启示的是:面对有“特殊规则”的题目,无需单独处理特殊情况,而是尝试将特殊规则融入通用逻辑中,实现代码的简洁性与高效性。后续遇到类似“字符串遍历+规则判断”的题目,可借鉴这种“相邻元素对比”的思路。