那天晚上加完班回家已经快十二点了,在地铁上刷题,刷到一个“整数转罗马数字”,整个人脑子都是 MCMXCIV… 顺便想起白天产品还问我:“能不能把版本号搞成罗马数字,看起来高级一点?”我心里想:你开心就好,但代码得我写对吧。
先把事儿说清楚,罗马数字这玩意儿其实不复杂,你就记住几个符号就行: 1 是 I,5 是 V,10 是 X,50 是 L,100 是 C,500 是 D,1000 是 M。 然后中间有点小骚操作: 4 不是 IIII,而是 5 前面加个 1,写成 IV; 9 不是 VIIII,而是 X 前面加个 1,写成 IX; 同理 40 = XL,90 = XC,400 = CD,900 = CM。 你看,其实就那几个坑,记住就完事了。
那问题来了,给你一个整数,比如 1994,你怎么一步步变成 MCMXCIV 呢? 你肯定不会想“从 1 开始加 I 加到吐血”,对吧,这太蠢了。 正常人肯定是:先用最大的数去“薅”它。
比如 1994: 先看 1000 能不能减,能,减完剩 994,对应先加一个 M。 然后 900 能不能减,能,再减,剩 94,加上 CM。 再看 90 能不能减,能,剩 4,加上 XC。 最后 4,刚好是特殊的 IV。 连起来就是:M + CM + XC + IV = MCMXCIV。 你仔细看这个过程,就是不断“从大到小匹配、能减就减、能贴就贴”。
所以核心思路特别简单一句话: 把所有“关键的数值”和“对应的罗马符号”提前排好,从大到小扫一遍,当前数字还大于等于这个值,就减掉它并把罗马符号拼上去,直到数字变成 0。
用代码写出来就很自然了。你不是让用 PHP 嘛,那咱就 PHP 版本的,直接可以丢到项目里用的那种:
<?phpfunctionintToRoman(int $num): string{// 这里把所有会用到的数字和罗马符号列出来// 一定要从大到小排,不然后面循环不好写 $values = [1000, 900, 500, 400,100, 90, 50, 40,10, 9, 5, 4,1 ]; $symbols = ["M", "CM", "D", "CD","C", "XC", "L", "XL","X", "IX", "V", "IV","I" ]; $res = "";// 贪心:从最大面额往下试for ($i = 0; $i < count($values) && $num > 0; $i++) { $value = $values[$i]; $symbol = $symbols[$i];// 当前这个面额能用多少次,就用多少次// 比如 1994 遇到 1000,就会进来一次while ($num >= $value) { $num -= $value; $res .= $symbol; } }return $res;}// 随便测几组var_dump(intToRoman(3)); // IIIvar_dump(intToRoman(4)); // IVvar_dump(intToRoman(9)); // IXvar_dump(intToRoman(58)); // LVIII = 50 + 5 + 3var_dump(intToRoman(1994)); // MCMXCIV
你整体看一眼,是不是很顺: 先把“所有可能用到的拼法”列出来,包括那些看起来有点怪的 4、9、40、90、400、900,全部当成“一个整体”处理。这样循环的时候就不用“特判各种情况”,逻辑反而更简单。
复杂度也别紧张,这题范围就 1~3999,而且我们 values 这个数组长度是固定的 13,每次 while 最多减几次就结束了,整体你就当常数时间,面试官问你复杂度,你直接说 O(1) 都没人会跟你抬杠。
顺带说一句,写这种题的时候,最容易翻车的点就两个: 一个是 values / symbols 没按从大到小排好,循环出来全是鬼东西; 另一个是漏了特殊情况,比如你没写 900、400 这种,那 900 就会被拆成 DCCCC,看起来就很怪。虽然从“数学上”说它还是 900,但绝大部分题目都会要求用标准写法。
我当时在地铁上敲完这段 PHP,回到家一跑,全对,心情好了一半。第二天产品再说“要罗马数字显高级”的时候,我就把这函数扔给他,说随便用,别再半夜改需求就行。
行,我先去喝口水,你要是想顺带搞个“罗马数字转整数”的反向版本,也可以在这基础上改一改,很类似。