罗马数字作为一种古老的计数系统,至今仍在钟表、书籍版权页等场景中使用。理解其转换规则不仅有助于解决LeetCode等编程题库中的经典问题,更能帮助我们理解不同数字表示系统背后的设计思想。本文将深入探讨罗马数字的构成规则,并详细解析如何用C++实现高效转换。
罗马数字由七个基本符号组成,每个符号对应特定的数值:
| 罗马字符 | 对应数值 |
|---|---|
| I | 1 |
| V | 5 |
| X | 10 |
| L | 50 |
| C | 100 |
| D | 500 |
| M | 1000 |
这些符号通过不同的组合方式表示更大的数值。例如:
罗马数字最特殊的规则是"减法表示法":当较小的数字出现在较大数字的左侧时,表示需要减去这个较小的数值。这种规则仅适用于以下六种特定组合:
| 组合 | 数值 | 解释 |
|---|---|---|
| IV | 4 | 5(V) - 1(I) |
| IX | 9 | 10(X) - 1(I) |
| XL | 40 | 50(L) - 10(X) |
| XC | 90 | 100(C) - 10(X) |
| CD | 400 | 500(D) - 100(C) |
| CM | 900 | 1000(M) - 100(C) |
理解这些特殊规则对正确实现转换算法至关重要。在实际应用中,这些规则避免了同一字符连续出现四次的情况(如IIII表示4),使数字表示更加简洁。
转换算法的核心思想是:从左到右遍历罗马数字字符串,比较当前字符与下一个字符的数值大小关系:
这种设计巧妙地处理了减法规则,无需预先识别所有特殊组合,只需在遍历时进行相邻字符的比较。
以下是完整的C++实现代码,包含详细注释:
cpp复制#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int romanToInt(string s) {
// 创建罗马字符到数值的映射表
unordered_map<char, int> romanMap = {
{'I', 1}, {'V', 5}, {'X', 10}, {'L', 50},
{'C', 100}, {'D', 500}, {'M', 1000}
};
int result = 0;
int n = s.size();
for (int i = 0; i < n; i++) {
int currentValue = romanMap[s[i]];
// 检查是否需要应用减法规则
if (i < n - 1 && currentValue < romanMap[s[i + 1]]) {
result -= currentValue;
} else {
result += currentValue;
}
}
return result;
}
哈希表初始化:
cpp复制unordered_map<char, int> romanMap = {
{'I', 1}, {'V', 5}, {'X', 10}, {'L', 50},
{'C', 100}, {'D', 500}, {'M', 1000}
};
使用unordered_map存储罗马字符到数值的映射,提供O(1)时间的查找效率。
核心逻辑判断:
cpp复制if (i < n - 1 && currentValue < romanMap[s[i + 1]]) {
result -= currentValue;
} else {
result += currentValue;
}
i < n - 1确保不会越界访问currentValue < romanMap[s[i + 1]]判断是否需要应用减法规则为了全面验证算法的正确性,我们设计了以下测试用例:
cpp复制int main() {
// 基础测试
cout << "III → " << romanToInt("III") << " (预期: 3)" << endl;
cout << "IV → " << romanToInt("IV") << " (预期: 4)" << endl;
cout << "IX → " << romanToInt("IX") << " (预期: 9)" << endl;
// 组合测试
cout << "LVIII → " << romanToInt("LVIII") << " (预期: 58)" << endl;
cout << "MCMXCIV → " << romanToInt("MCMXCIV") << " (预期: 1994)" << endl;
// 边界测试
cout << "I → " << romanToInt("I") << " (预期: 1)" << endl;
cout << "MMMCMXCIX → " << romanToInt("MMMCMXCIX") << " (预期: 3999)" << endl;
return 0;
}
以"MCMXCIV"(1994)为例,逐步解析算法执行过程:
| 字符 | 当前值 | 下一字符 | 下一值 | 比较结果 | 操作 | 累计结果 |
|---|---|---|---|---|---|---|
| M | 1000 | C | 100 | > | +1000 | 1000 |
| C | 100 | M | 1000 | < | -100 | 900 |
| M | 1000 | X | 10 | > | +1000 | 1900 |
| X | 10 | C | 100 | < | -10 | 1890 |
| C | 100 | I | 1 | > | +100 | 1990 |
| I | 1 | V | 5 | < | -1 | 1989 |
| V | 5 | - | - | - | +5 | 1994 |
除了哈希表方案,还可以使用switch-case实现字符到数值的映射:
cpp复制int romanCharToInt(char c) {
switch(c) {
case 'I': return 1;
case 'V': return 5;
case 'X': return 10;
case 'L': return 50;
case 'C': return 100;
case 'D': return 500;
case 'M': return 1000;
default: return 0;
}
}
这种实现可能比哈希表稍快,但代码可维护性稍差。在实际项目中,应根据具体需求选择合适方案。
边界条件处理:
i < n - 1导致数组越界特殊组合识别:
调试建议:
在实际应用中,我们可能需要先验证输入的罗马数字是否合法。一些验证规则包括:
反向问题(整数转罗马数字)同样有趣。基本思路是:
罗马数字系统起源于古罗马,最初采用加法表示法。减法表示法是后来发展出的简化形式。了解这些历史背景有助于更好地理解其设计原理。
在实际编码面试中,罗马数字转换是经典的字符串处理问题。我总结了以下几点经验:
罗马数字转换问题虽然看似简单,但很好地考察了对规则的准确理解、字符串处理能力以及边界条件处理能力。掌握这类基础算法问题,对提升编程思维和解决更复杂问题大有裨益。