1 Roman to Integer
Given a roman numeral, convert it to an integer.
Input is guaranteed to be within the range from 1 to 3999.
首先,想做出这道题得了解一下罗马数字。罗马数字有如下符号:
- I(1)
- V(5)
- X(10)
- L(50)
- C(100)
- D(500)
- M(1000)
【左减右加】如果当前比前一个大,说明这一段的值应该是当前这个值减去前面记录下的临时变量中的值。比如IIV = 5 – 2。 如果当前比前一个小,那么就可以先将临时变量的值加到结果中,然后开始下一段记录。比如VI = 5 + 1。
【右加连续的数字重复不得超过三次,左减的数字只有一个】VIII = 8,但9用IX表示,而不是VIIII。900 用 CM 表示,800 用 DCCC 表示,而不是 CCM。
【大的数字在左边】对于MCMXCVI,我们可以很快把它分解成M + CM + XC + VI,即1000 + (1000-100) + (100-10) + (5+1) = 1996。为什么知道这样分解?
1.1 一般思路
由于此题只考虑1 to 3999,所以我们可以这样理解:罗马数字没有进位制,它仅仅是几个基本字母所代表的数字的和而已,所以,罗马数字总体上是在做加法,只是有一个【左减】的特殊情况。【左减】的模式是什么?【小 大】,如例子中的 CM 、 XC。
所以,我们从左往右遍历,如果当前字母(的数值)与后一个字母相等或是大于的话,则加上当前字母的值,开始判断一个字母:
DCCC,curr = D,next = C,D > C, num 加上D,i++开始判断C的情况。
如果当前字母小于后一个字母,则看是【小 大】的特殊情况,num加上(next - curr)的值,i+2:
MDMV,curr = D,next = M, D < M,num + (M - D),i+2,跳过M,判断V的情况。
这一段逻辑的代码为:
if(s.length() == 1)
return toNumber(s.charAt(0));
int num = 0;
int curr, next;
for(int i=0; i<s.length()-1;){
curr = toNumber(s.charAt(i));
next = toNumber(s.charAt(i+1));
// 大 小
if(curr >= next){
num += curr;
i++;
}
// 小 大
else {
num += (next - curr);
i += 2;
}
}
但是这个逻辑有一个问题,我们每次都是以curr为核心,判断curr与next的关系。如果取到数组的末尾,也就是最后两个数字,如果是【大小 大】的情况那还好,两个会结合在一起运算;如果是【大 小】或是【相等】,结果num只会加上倒数第二个的值,最后一个字母没法判断了,所以,我们还得对数组的末尾两个字母做额外的判断:
int last = toNumber(s.charAt(s.length()-1));
int last2 = toNumber(s.charAt(s.length()-2));
if(last <= last2){
num += last;
}
1.2 代码改进
上面的思路是不是有点繁琐?
其实这就又归结为数组的边界处理问题了。在需要用到数组的算法题中,我们每次都要格外小心数组为空,数组只有一个元素,和数组边界的问题。
既然从第一个元素开始遍历,每次判断它与下一个元素的方式,在末尾不可避免地要多做一次判断,那么我们很自然地可以想到换一种思路:从第二个元素开始遍历,每次判断它与前一个元素的关系。接下来,我们试一试这种思路是否可行,不行的话就算了。
还是拿 MCMXCVI 举例。从第二个数字C开始判断:
- MC, pre = M,curr = C,【大 小】的关系,所以num加上curr。
- CM, pre = C, curr = M,【小 大】的特殊关系,理应num加上(curr - pre),但是前面错误地将C加了一次了,所以修正为加上(curr - 2 * pre)。
- MX,【大 小】,加上X。
- XC,【小 大】,加上(C - 2*X)。
- CV,【大 小】,加上V。
- VI,【大 小】,加上I。
对于首字母我们怎么处理呢?上面的例子中,首字母M之间加上去就好了,那么我们再来看看特殊情况是不是也可以这样。以IV(4)举例,首字母直接加上,num = 1;判断IV为【小 大】,num + (V - 2 * I),结果正确,可以。所以代码为:
int num = toNumber(s.charAt(0));
int pre, curr;
for(int i=1; i<s.length(); i++){
curr = toNumber(s.charAt(i));
pre = toNumber(s.charAt(i-1));
if( curr > pre){
num += (curr - 2 * pre);
}
else {
num += curr;
}
}
这种思路不仅不用额外地考虑边界问题,而且意外地,我发现它还不用额外地去判断罗马数字只有一个字母的情况。
1.3 方法三
从右王左遍历:
int num = toNumber(s.charAt(s.length()-1));
//从右往左遍历
int curr, pre;
for(int i= s.length()-2; i>=0;i--){
curr = toNumber(s.charAt(i));
pre = toNumber(s.charAt(i+1));
if(curr < pre){
num -= curr;
}
else {
num += curr;
}
}
2 Integer to Roman
在一开始我就举了一个例子:对于MCMXCVI,我们可以很快把它分解成M + CM + XC + VI,即1000 + (1000-100) + (100-10) + (5+1) = 1996。
在来一个:MDCCCLXXXIV = M + DCCC + LXXX + IV = 1000 + 800 + 80 + 4 = 1884。
发现规律没有? 现在十进制在人类世界占主流是有一定原因的,可以因为它符合人类的大脑内部回路吧。罗马数字毕竟也是人类为生产生活创造的,所以它和十进制的联系还是比较直观的。
1~9: {“I”, “II”, “III”, “IV”, “V”, “VI”, “VII”, “VIII”, “IX”};
10~90: {“X”, “XX”, “XXX”, “XL”, “L”, “LX”, “LXX”, “LXXX”, “XC”};
100~900: {“C”, “CC”, “CCC”, “CD”, “D”, “DC”, “DCC”, “DCCC”, “CM”};
1000~3000: {“M”, “MM”, “MMM”}.
1996 转 罗马数字: 1996 = 1000 + 900 + 90 + 6 = M + CM + XC + VI。
好了,现在可以直接贴代码了:
public class Solution {
public String intToRoman(int num) {
//纬度1:0个、1十、2百、3千
//纬度2:0-9,其中0没有用
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"}
};
String s="";
int mod;
int d = 0;
while(num != 0){
mod = num % 10;
s = roman[d][mod] + s;
d++;
num /= 10;
}
return s;
}
}