一道有趣的题 – 将分数转成小数
题目
写一个函数,入参是一个分子和一个分母,都是int型,返回值是一个String类型,代表该分数的小数。
若该小数有循环部分,则循环部分用括号括起来。
假设分母必定不为0.
public String fractionToDecimal(int a, int b)
举例
假设a是分子,b是分母
Case-1
Input: a=1, b=2
Output: "0.5"
Case-2
Input: a=2, b=1
Output: "2"
Case-3
Input: a=4, b=333
Output: "0.(012)"
思考
这道题首先应该思考的是,在数学上是怎么实现分数转小数的,即,竖式除法是怎么做的。然后再把它转成程序。
这里有个难点是循环部分怎么识别和处理?所以我们先不看这个case,先从简单的开始,即Case-1.
对于竖式除法,不够除就填0,然后相当于把被除数扩大10倍再接着除;等到够除了,就先填一个商,然后求出余数,接下来再把余数乘以10,再接着除。一直除到余数为0,或余数出现循环部分为止。
说起来似乎有点赘述的感觉。画个表格就很好理解了。
r * 10 | 被除数 a | 除数 b | 商 c | 余数 r |
---|---|---|---|---|
r * 10 | 1 | 2 | 0 | 1 |
r * 10 | 10 | 2 | 5 | 0 |
通过上表,我们把商的部分连接起来,就是"0.5"了。
好了,工具有了,现在来看那个循环的例子 Case-3
r * 10 | 被除数 a | 除数 b | 商 c | 余数 r |
---|---|---|---|---|
r * 10 | 4 | 333 | 0 | 4 |
r * 10 | 40 | 333 | 0 | 40 |
r * 10 | 400 | 333 | 1 | 67 |
r * 10 | 670 | 333 | 2 | 4 |
r * 10 | 40 | 333 | 0 | 40 |
r * 10 | 400 | 333 | 1 | 67 |
r * 10 | 670 | 333 | 2 | 4 |
从上表中, 把商的数字从上往下连起来,就是0012012…,写成无限循环小数就是 “0.(012)”.
怎么决定循环部分呢?
仔细观察上表,会发现,当余数第一次出现和之前重复的时候,就是出现循环了。
那么这个循环的起始位置是在哪里呢?
还是仔细观察上表:循环的起点位置是在所重复的余数第一次出现的位置下一个位置(即上表的第2行),循环的终点位置是所重复的余数第二次出现的位置(即上表的第4行)。
由以上分析,我们也就能够得知,画这个表是什么时候结束了。两种情况皆可结束:一是余数为0;二是余数第一次出现重复的时候。
画上表的过程也就是程序处理的过程。
先写一点伪代码。
int a = xxx, b = yyy;
c = a / b;
r = a - b * c;
if (r == 0) {
// generate result
...
return result;
}
while (true) {
a = r * 10;
c = a / b;
r = a - b * c;
if (r == 0 || r occurs again) {
// generate result
...
return result;
}
}
最后一个小问题,也是上述伪代码没有解答的问题:怎么知道r重复了呢?又怎么知道上一个一样的r的位置呢(用来写左括号)?
稍有点编程功力的应该很容易想到:
- 用一个列表记录下上表中的所有商c
- 用一个HashMap记录下所有余数r以及当时它们在表中的位置(即处于第几行),即key为r,value为位置
好了,主要问题都解决了。还有什么遗漏呢?
如果按照这样写出程序了,至少还有2点遗漏:
- 正负问题:所返回的String可能应该带一个负号
- 溢出问题:因为
a = r * 10
, 这就可能造成被除数a超出了int型的范围
这2个问题都很好解决。不再赘述。
完整的实现代码如下:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FractionToDecimal {
public String fractionToDecimal(int numerator, int denominator) {
long a = numerator, b = denominator;
boolean positive = (a>=0 && b>0) || (a<=0 && b<0);
if (a < 0) a = -a;
if (b < 0) b = -b;
long c = a / b;
long r = a - b * c;
if (r == 0) {
return positive ? Long.toString(c) : "-" + c;
}
int index = 0;
List<Long> quotientList = new ArrayList<>(List.of(c));
Map<Long, Integer> remainderMap = new HashMap<>(Map.of(r, index)); // r -> index of c
while (true) {
a = r * 10;
c = a / b;
r = a - b * c;
quotientList.add(c);
index ++;
if (r == 0 || remainderMap.containsKey(r)) break;
remainderMap.put(r, index);
}
StringBuilder sb = new StringBuilder();
if (!positive) sb.append("-");
sb.append(Long.toString(quotientList.get(0)))
.append(".");
int firstLoopRemainderIndex = -1;
if (r != 0) {
firstLoopRemainderIndex = remainderMap.get(r);
if (firstLoopRemainderIndex == 0) {
sb.append("(");
}
}
for(int i=1; i<quotientList.size(); i++) {
sb.append(Long.toString(quotientList.get(i)));
if (i == firstLoopRemainderIndex) {
sb.append("(");
}
}
if (r!=0) sb.append(")");
return sb.toString();
}
public static void main(String[] args) {
FractionToDecimal ftd = new FractionToDecimal();
int a = 1;
int b = 2;
System.out.printf("%d / %d = %s%n", a, b, ftd.fractionToDecimal(a, b));
a = 4;
b = 333;
System.out.printf("%d / %d = %s%n", a, b, ftd.fractionToDecimal(a, b));
a = 40;
b = 333;
System.out.printf("%d / %d = %s%n", a, b, ftd.fractionToDecimal(a, b));
a = -6;
b = 5;
System.out.printf("%d / %d = %s%n", a, b, ftd.fractionToDecimal(a, b));
a = 1;
b = 6;
System.out.printf("%d / %d = %s%n", a, b, ftd.fractionToDecimal(a, b));
a = 10;
b = 23;
System.out.printf("%d / %d = %s%n", a, b, ftd.fractionToDecimal(a, b));
}
}
运行结果为:
1 / 2 = 0.5
4 / 333 = 0.(012)
40 / 333 = 0.(120)
-6 / 5 = -1.2
1 / 6 = 0.1(6)
10 / 23 = 0.(4347826086956521739130)
(END)