昨天晚上十一点多,我在公司楼下便利店门口喝着酸奶,准备回去把一个 bug 收个尾,我们组那个小李突然微信丢给我一题,说“哥,整数转罗马数字你会写不”。我当时脑子一抽,还以为让我背“IVXLCDM”口诀,结果一看,是面试常考的那个算法题。
题目大概意思就是:给你一个正整数,比如 1994,把它变成罗马数字字符串,比如 "MCMXCIV"。一般约定输入范围是 1 到 3999,超过这个范围罗马人自己都懒得管了,对吧。
先把罗马数字这套“古人协议”说清楚,不然写代码全是懵的。常用的七个符号:
但它又不是单纯的加加加,还有减法规则,比如: 4 不是 "IIII" 而是 "IV"(小的在前面减掉), 9 是 "IX", 40 是 "XL", 90 是 "XC", 400 是 "CD", 900 是 "CM"。
也就是说,你得同时处理“普通”的和值,还有这几个特殊组合,不然就会把 9 转成 "VIIII",面试官直接给你一个大大的问号。
我当时跟小李说:这个题别想复杂,最舒服的写法就是“贪心”,从最大的罗马面值往下减。就有点像你去买奶茶,先尽量用 100 块,剩下再用 50,再 20,再 10,直到找不开为止。
具体做法就是准备两个数组,对应好数字和字符串:
- 数组里的数字:1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1
- 对应的罗马串:"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"
然后从左往右扫一遍,每次看当前 num 还能减多少次这个数,能减就减,同时把对应的罗马串拼上去。这样一轮下来,num 就变成 0 了,答案也就出来了。因为这个数组是写死的,长度固定,复杂度就相当于 O(1),跑得飞快。
我用 js 给他敲了一版,大概长这样:
// 整数转罗马数字functionintToRoman(num) {// 安排好从大到小的数值和罗马数字const values = [1000, 900, 500, 400,100, 90, 50, 40,10, 9, 5, 4, 1 ];const symbols = ["M", "CM", "D", "CD","C", "XC", "L", "XL","X", "IX", "V", "IV", "I" ];let res = "";// 从最大面值开始“贪心”地减for (let i = 0; i < values.length && num > 0; i++) {const value = values[i];const symbol = symbols[i];// 这个 while 很关键,同一个面值可能要用多次while (num >= value) { num -= value; res += symbol; } }return res;}// 简单测几下console.log(intToRoman(3)); // "III"console.log(intToRoman(58)); // "LVIII"console.log(intToRoman(1994)); // "MCMXCIV"
你看一下比如 58 这个例子,脑子里过一遍就更清楚了:
- 58 < 1000、900、500...一直跳到 50
- 58 >= 50,于是 num -= 50 变成 8,结果串 += "L"
- 8 >= 5,于是 num = 3,结果串 += "V"
- 然后 3 >= 1,一口气拼三个 "I",最后得到 "LVIII"
这个过程就完全是我们数组里从大到小扫一遍,不断减、不停拼的一个循环,逻辑很直观,小李当场就“哦——这样啊”那种表情。