罗马数字转换器 (Roman Numeral Converter)
题目链接
问题解释
基本解法
思路提示
- 这道题其实是有一点难度的,至少我在第一次尝试的时候花了些时间
- 我们先来梳理一些基本概念,和逻辑思路。只有这些想清楚了,才能开始写代码
- 首先,罗马数字通过以下几个字母来表示:
数字 | 字符 |
---|---|
1 | I |
5 | V |
10 | X |
50 | L |
100 | C |
500 | D |
1000 | M |
- 我们可以先通过罗马数字转数字的逻辑来反推这道题目的解题思路,转换时,对于相邻的字母,如果左边比右边大就相加,右边比左边大就相减
- 比如,
"CX"
表示110
,而"XC"
表示90
- 比如,
- 现在,我们至少可以有一条基本思路。对于给定的数字,只需要按照上面的对应关系从大到小进行比较,匹配到了(小于等于)就得出罗马数字字符,然后原数减去匹配数字,再继续匹配就可以了
- 举个例子,给出数字为
100
,它小于1000
和500
。比较到100
发现它们相等,因此得出C
- 如果给出
120
,还是先获取到"C"
,然后减去100
得到20
。那么20
根据上面的对应关系匹配到10
,因此得到"X"
。再减去10
,也是匹配到"X"
。因此,结果是"CXX"
- 举个例子,给出数字为
- 下一步,就是考虑一些特殊情况,这里有一个很重要的原则:同一罗马数字字符,最多连续出现三次。引申出一条原则:如果右边比左边大,那么左边只可以有一位数
- 听起来好像很复杂,来看例子。对于
4
,不能转换成"IIII"
,而应为"IV"
- 对于
8
,不可以转换成"IIX"
,而应为"VIII"
- 听起来好像很复杂,来看例子。对于
- 进一步考虑,我们会发现,其实特殊情况就是罗马字符相减,或者说左边比右边小的情况。比如对于
8
,转换逻辑与上面提到的基本思路是完全相符的。但9
是不符合的,因为按照一开始的思路,9
会被转换成"VIIII"
。这条结论十分重要,如果没看懂,请再读一遍。特殊情况列表如下:
数字 | 字符 |
---|---|
4 | IV |
9 | IX |
40 | XL |
90 | XC |
400 | CD |
900 | CM |
- 这时候你可能会有疑问,为什么
99
不算特殊情况?这就涉及到另一条规则,如果左边比右边小,那么左右两个罗马数字字符只可以跨一位。意思是,比如对于"I"
,可以有"IV"
和"IX"
,但不会有"IC"
- 对于
99
,转换思路是,先转换90
,得到"XC"
。再转换9
,得到"IX"
。因此结果是"XCIX"
,而不是"IC"
。这个结论,是解决这道题目的关键 - 同理,对于
999
,也应该是先转换900
,再转换90
,再转换9
- 对于
- 以上就是转换的基本思路。剩下的就是写代码了
参考链接
- 罗马数字 维基百科页面
- 除此之外,不需要其他参考,只要你会写循环、会四则运算就行
伪代码(逻辑思路)
- 由于这道题代码看起来会比较复杂,因此先给出伪代码。如果你已经理解了上面的思路,那么你可以参考以下的逻辑思路,来试着自己写一下
- 整体上,我们才用
num
来进行循环。只要找到对应的罗马字符,我们就把num
缩小,同时把对应的罗马字符添加到结果。显然,循环的跳出条件就是num === 0
- 如果你在写的过程中遇到问题,请通过在代码中加上
console.log
来调试,或者再回顾一下上面的思路详解
1 | # 生成一个用于判断的数组,[1, 5, 10, 50, 100, 500, 1000] |
代码
1 | function convert(num) { |
解释
- 再回顾一遍整体思路:
- 以
num > 0
为跳出条件进行循环 - 对于
num
,通过numArr
来逐个判断。如果大于当前元素,就表示匹配到,可以进行转换了 - 进一步判断特殊值,如果是特殊值,就按照特殊值方式转换,否则就按标准思路转换
- 以
- 其实,唯一需要解释的就是
[i + 1, i % 2 ? i - 1 : i]
那里吧。我们先看看它所在的if
区域是干什么的 - 这个
if
处于遍历数组的过程中,作用是判断特殊值。如果你不明白上面的这个写法,请你和我一样,列出来这样的一个表格:
当前元素 | 索引(index) | 特殊值 | 特殊值计算方式 | 特殊值计算索引 |
---|---|---|---|---|
1000 | 6 | 无 | 无 | 无 |
500 | 5 | 900 | 1000 - 100 | 6, 4 |
100 | 4 | 400 | 500 - 100 | 5, 4 |
50 | 3 | 90 | 100 - 10 | 4, 2 |
10 | 2 | 40 | 50 - 10 | 3, 2 |
5 | 1 | 9 | 10 - 1 | 2, 0 |
1 | 0 | 4 | 5 - 1 | 1, 0 |
- 仔细看第二列,也就是当前索引与最后一列的关系。设当前索引为
i
,那么最后一列要么是(i + 1), (i)
,要么是(i + 1), (i - 1)
- 进一步观察可以发现,除了索引为
6
的情况,其他情况都符合:当索引i
为奇数时最后一列是(i + 1), (i - 1)
,当索引为偶数时最后一列是(i + 1), (i)
- 代码中的
checkerIndex
就是表格的最后一列,特殊值计算索引;checkerNum
就是求差之后的特殊值,也就是表格的第三列。需要注意的是,元素1000
没有特殊值需要检查,因此我们要加上i < 6
的判断 - 这个解法可以算是暴力解法了。但能做的优化我还是尽力做了的。比如,不需要每次都判断所有的特殊值,而是动态地生成特殊值,只检查当前情况下的特殊值
- 这个解法,效率一定是不高的,时间复杂度达到了
n
平方。只是这个解法不需要太多的技巧,相信只要你理解了一开始说的思路,知道要做特殊情况判断,写出来这个答案还是不成问题的
优化 - 优化初始值定义
思路提示
- 既然判断特殊值这么麻烦,而且特殊值也就那几个,为什么不把它们也写进数组中呢?比如特殊值
40
,我们完全可以把它定义在10
与50
之间,因为这个40
是数字小于50
并且大于10
的时候需要判断的,那我们在10
之前先判断就好了 - 有的朋友可能会想到,为什么不用
Object
去定义呢?这样就不用定义两个数组了。但请注意,对象存储的是一一对应关系,而不是顺序。JavaScript 中遍历对象的方法你肯定知道,那就是for...in
,即使是 ES6 的for...of
也不能保证按”顺序”输出对象中的元素,因为,Object
本身就不存在顺序的概念 - 而遍历数组,我们可以用
for
、while
、.forEach()
或者for...of
。这些一定是按照一定顺序的。所以还是得用两个数组,然后通过索引来建立对应关系 - 有朋友可能会说,”我用 Object,测试通过了噢”,”为什么不能用?在浏览器控制台中也是按’顺序’输出的”。那我们进一步展开来讲。如果对象的
key
是唯一类型的,那么确实,for...of
会按定义的顺序输出。如果对象的key
是包含多种类型的(比如数字和字符串),而且定义的顺序也是混的,那么输出就不一定会按照原来的顺序了 - 这道题目中,显然我们的
key
只有数字(注意,用数字作为对象的key
,会被自动转换为字符串,你可以试试),之所以我不打算才用对象来写,就是防止产生 “对象是有顺序的” 这样的误会
代码
1 | function convert(num) { |
解释
- 思路还是跟之前的一样,没有任何性能方面的优化,但至少不用去写复杂的逻辑判断了
优化 - 封装函数
思路提示
- 说到封装,还记得 “DRY” 原则么?请查看上一片博文,里面提到过
- 先来看看,需不需要封装。举个例子,对于
30
这样的数,我们需要进行 3 次判断、添加字符串和减去10
这样的重复操作 - 进一步思考,对于任何需要转换的数,我们都需要进行判断、添加字符串和减去特定值的操作。因此,由于存在这样的重复操作,至少封装是可以进行的
- 既然决定要封装,那么下一步就要定好传入的参数和返回值。既然原先的输入是数字,我们最终要输出字符串,很显然,封装好的参数也应该是这样:输入数字,输出字符串
- 继续考虑,我们可以举一个例子。比如传入的是
30
,那我们应该输出"XXX"
。按照原先的思路,30
是在遍历中匹配10
的。因此,这里我们不难推导出输出就是重复30 / 10 = 3
次10
的对应字符串"X"
- 接下来考虑特殊情况,按照上一条的思路考虑
- 对于
40
来说,输出的就会是"XXXX"
,当然这个结果不是我们想要的。因此,我们需要把前三个或后三个"X"
去掉,并在结尾补上更高一级的"L"
- 比如
90
不应为"LXXXX"
,而应该是"XC"
。在这种情况下,显然我们要去掉的是前四个"LXXX"
- 再比如,
4
不应为"IIII"
是"IV"
,所以我们要去掉前三个"I"
- 对于
- 综上所述,遇到特殊值,我们处理的方式就是:保留最后一个罗马字符,并在后面添加更高级的罗马字符
- 以下代码,我用到了递归写法,供大家参考。由于是递归调用,不仅需要遍历过程中当前的元素和索引,还需要当前已生成的罗马字符。当然,如果你愿意,也可以把这个结果存到函数外部。只是我个人更喜欢写到参数里面去
- 就算不用递归,一样是可以写成的,请大家尝试一下
参考链接
伪代码(逻辑思路)
1 | # 初始化不含特殊条件的数字数组与罗马字符对应数组 |
代码
1 | function convert(num) { |
解释
- 要解释的,都在上面的伪代码和代码注释中。如果你有哪里不明白的,或者有更好的建议,请在下方评论留言
- 如果你觉得我写的方法漏洞很大,或者有很明显的优化空间,也请在下方评论指出,不胜感激