1. 罗马数字基础与转换规则解析
罗马数字系统起源于古罗马,采用七个基本符号的组合来表示数值。这七个符号及其对应的阿拉伯数字值如下:
| 罗马符号 | 对应数值 |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
罗马数字的构建遵循三个核心原则:
-
加法原则:当较小符号出现在较大符号右侧时,将符号对应的数值相加。例如:
- VI = 5 + 1 = 6
- LX = 50 + 10 = 60
- DC = 500 + 100 = 600
-
减法原则:当较小符号出现在较大符号左侧时,用大数减去小数。这种形式仅适用于特定组合:
- IV = 5 - 1 = 4
- IX = 10 - 1 = 9
- XL = 50 - 10 = 40
- XC = 100 - 10 = 90
- CD = 500 - 100 = 400
- CM = 1000 - 100 = 900
-
重复限制:
- I、X、C、M可以连续出现最多3次(如III=3,XXX=30)
- V、L、D不能重复(不存在VV表示10的情况)
注意:罗马数字没有表示零的符号,这也是为什么题目限制输入数字范围在1-3999之间。古罗马人在商业记录中使用"NULLA"表示零,但这不属于标准罗马数字系统。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法设计与实现思路
2.1 问题分解策略
将整数转换为罗马数字的核心思路是贪心算法的应用——每次尽可能使用最大的符号值来表示当前数值。具体实现可以采用两种主流方法:
-
硬编码查表法(本文实现方法):
- 预先建立各位数字的罗马表示映射表
- 将输入数字按位分解后查表拼接
- 时间复杂度O(1),空间复杂度O(1)
-
贪心匹配法:
- 维护一个从大到小的值-符号对列表
- 循环减去最大可表示的符号值
- 时间复杂度O(1),空间复杂度O(1)
本文选择硬编码查表法,原因在于:
- 数字范围明确(1-3999),可以穷举所有可能的位表示
- 代码更直观,易于理解和维护
- 实际运行效率略高于贪心匹配法(减少循环判断)
2.2 数据结构设计
我们使用一个二维数组roman来存储各位数字的罗马表示:
java复制String[][] roman = {
{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}, // 个位
{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}, // 十位
{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}, // 百位
{"", "M", "MM", "MMM"} // 千位
};
这种设计有三大优势:
- 直接映射:数组索引对应数字位值,如
roman[0][4]直接返回个位数字4的表示"IV" - 边界安全:空字符串""处理了某位为零的情况
- 扩展性:若需支持更大数字,只需扩展千位数组
3. 代码实现与逐行解析
3.1 完整代码实现
java复制class Solution {
public String intToRoman(int num) {
// 定义各位罗马数字表示表
String[][] roman = {
{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}, // 个位
{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}, // 十位
{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}, // 百位
{"", "M", "MM", "MMM"} // 千位
};
// 分解数字各位
int thousand = num / 1000;
int hundred = (num % 1000) / 100;
int ten = (num % 100) / 10;
int unit = num % 10;
// 拼接最终结果
StringBuilder sb = new StringBuilder();
sb.append(roman[3][thousand])
.append(roman[2][hundred])
.append(roman[1][ten])
.append(roman[0][unit]);
return sb.toString();
}
}
3.2 关键步骤详解
-
数字分解:
thousand = num / 1000:获取千位数字(整除1000)hundred = (num % 1000) / 100:先取模得到后三位,再整除100得到百位ten = (num % 100) / 10:取模得到后两位,整除10得到十位unit = num % 10:直接取模得到个位
-
查表拼接:
- 使用
StringBuilder高效拼接字符串 - 按千、百、十、个位顺序查表:
roman[3][thousand]:千位(第4维数组)roman[2][hundred]:百位(第3维数组)roman[1][ten]:十位(第2维数组)roman[0][unit]:个位(第1维数组)
- 使用
性能提示:在Java中,StringBuilder比直接字符串拼接(+操作符)效率更高,特别是在循环或多次拼接场景下。虽然本题中性能差异不大,但养成使用StringBuilder的习惯对编写高效代码很重要。
4. 测试用例与边界分析
4.1 标准测试用例
| 输入数字 | 预期输出 | 验证要点 |
|---|---|---|
| 3 | III | 最小重复次数 |
| 4 | IV | 减法规则 |
| 9 | IX | 减法规则 |
| 58 | LVIII | 多位数组合 |
| 1994 | MCMXCIV | 复杂组合 |
| 3999 | MMMCMXCIX | 最大值边界 |
4.2 边界情况处理
-
最小值边界:
- 输入1应返回"I"
- 代码中个位数组已包含1的映射
-
最大值边界:
- 输入3999返回"MMMCMXCIX"
- 千位数组最大到3("MMM")
- 其他位通过减法规则组合
-
中间值验证:
- 如2024应返回"MMXXIV"
- 验证各位组合是否正确
调试技巧:在IDE中设置以下测试用例断点,单步执行观察变量变化:
- num = 2946 → 预期输出"MMCMXLVI"
- 检查thousand=2, hundred=9, ten=4, unit=6
- 验证每步查表结果是否正确
5. 算法优化与变体
5.1 贪心算法实现
作为对比,我们看看贪心算法的实现方式:
java复制class Solution {
public String intToRoman(int num) {
int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] symbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values.length && num > 0; i++) {
while (num >= values[i]) {
num -= values[i];
sb.append(symbols[i]);
}
}
return sb.toString();
}
}
两种方法的对比:
| 维度 | 查表法 | 贪心法 |
|---|---|---|
| 时间复杂度 | O(1) | O(1) |
| 空间复杂度 | O(1) | O(1) |
| 代码可读性 | 更直观 | 稍抽象 |
| 扩展性 | 需修改数组 | 只需扩展值-符号对 |
| 实际运行速度 | 略快(约5%) | 略慢 |
5.2 扩展思考:罗马数字转整数
作为反向操作,罗马数字转整数的算法也值得了解:
java复制class Solution {
public int romanToInt(String s) {
Map<Character, Integer> map = new HashMap<>();
map.put('I', 1);
map.put('V', 5);
map.put('X', 10);
map.put('L', 50);
map.put('C', 100);
map.put('D', 500);
map.put('M', 1000);
int result = 0;
int prev = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int curr = map.get(s.charAt(i));
result += (curr < prev) ? -curr : curr;
prev = curr;
}
return result;
}
}
关键点:
- 从右向左处理字符串
- 当前值小于前值时做减法(如IV中的I)
- 时间复杂度O(n),n为字符串长度
6. 常见问题与解决技巧
6.1 典型错误案例
-
减法规则遗漏:
java复制// 错误实现:未处理4/9特殊情况 if (hundred == 4) { sb.append("CCCC"); // 应该用CD } -
重复次数错误:
java复制// 错误:V不能重复 sb.append("VV"); // 表示10应该用X -
位分解错误:
java复制// 错误:百位计算遗漏取模 int hundred = num / 100; // 当num=1234时会得到12
6.2 调试技巧
-
打印中间结果:
java复制System.out.println("千位:" + thousand + " → " + roman[3][thousand]); -
单元测试建议:
java复制@Test public void testIntToRoman() { assertEquals("III", intToRoman(3)); assertEquals("LVIII", intToRoman(58)); assertEquals("MCMXCIV", intToRoman(1994)); } -
边界测试:
java复制// 测试最小值 assertEquals("I", intToRoman(1)); // 测试最大值 assertEquals("MMMCMXCIX", intToRoman(3999));
6.3 性能优化
虽然本题的两种解法时间复杂度都是O(1),但在实际编码竞赛中还可以考虑:
-
使用switch-case替代数组(适用于固定输入范围):
java复制String getUnitRoman(int num) { switch(num) { case 1: return "I"; case 4: return "IV"; // ... } } -
预计算所有可能(空间换时间):
java复制String[] precomputed = new String[4000]; // 初始化时计算1-3999的所有罗马数字 -
位运算加速:
- 用移位代替除法(现代编译器通常会自动优化)
- 但会降低代码可读性,实际收益不大
在实际工程中,本文的查表法已经是最佳实践,建议不要过度优化破坏代码可读性。
