算法的力量
有这么一个数,当把它的最后一位(个位)挪到第一位的时候,得到的新数刚好是原来数的两倍。问这个数是多少?
——出自 1985 出版的一本小学5年级学生用的数学课外读物——《儿童数学世界》
这个问题看似简单,就是要找一个数出来,把这个数个位上的数字挪到最前面去,例如 123 变成 312,12345变成51234。但是还要求得到的“新数”要是原来数的两倍。
简单的分析一下这条业务规则,不难得出下面的结论:
1. 取一个数作为“原数”;
2. 把“原数”个位上的数字挪到最前面,保存为一个“新数”;
3. 比较两个数字,如果“新数”是“原数”的两倍,则打印两个数并退出程序;
4. 如果不符合要求,则原数自加1并回到步骤2。
显然通过手工方式找到这个数是不太现实的,为了加快查找这个数的速度,让我们编写一段代码来提高工作效率。已经实现的代码如下:
1#~ defined a method to move the last number to line-begin
2
3def get_new_number(original_number)
4
5
6
7 #~ get last number
8
9 last_number = original_number%10
10
11 puts "the last_number is : "+last_number.to_s
12
13
14
15 #~ get the length of original_number
16
17 original_number_in_sting=original_number.to_s
18
19 puts "the length of original_number is : "+original_number_in_sting.length.to_s
20
21
22
23 #~ set the original_number = original_number/10
24
25 original_number = original_number/10
26
27 puts "the new original_number is : "+original_number.to_s
28
29
30
31 #~ move the last number to line-begin of original_number
32
33 for counter in 2..original_number_in_sting.length
34
35 last_number=last_number*10
36
37 end
38
39 puts "the new last_number is : " + last_number.to_s
40
41
42
43 #~ return the new number
44
45 return last_number+original_number
46
47
48
49end
50
51
52
53
54
55#~ initialization
56
57#~ set the variable original_number = a number that the number >11
58
59original_number = 1
60
61puts "First: the original_number is : " + original_number.to_s
62
63#~ set the variable new_number = get_new_number(number01)
64
65new_number = get_new_number(original_number)
66
67puts "First: the new_number is : "+new_number.to_s
68
69#~ finished initialization
70
71
72
73
74
75while original_number*2 != new_number
76
77
78
79 original_number = original_number + 1
80
81 puts "the original_number in loop is : "+original_number.to_s
82
83
84
85 new_number = get_new_number(original_number)
86
87 puts "the new_number in loop is : "+new_number.to_s
88
89
90
91end
92
93
94
95puts "We’ve got the number! It is : "+original_number.to_s
上面的代码是在 Ruby 1.8.4 下面调试通过的。这是典型的完全通过分析业务规则并忠实于业务规则而实现的一段代码,其中定义了一个方法专门来处理“把个位的数字挪到最前面生成一个新数”这件事情,其他部分就是不断地反复比较、尝试,直到找到我们所期望的那个数。这段代码也并不复杂,其中“#~ ”表示注释掉的内容,puts 表示打印信息在屏幕上。如果你有兴趣可以很容易的用其他语言改写。
如果说上面的分析和代码实现是使用了“业务视角”的话,下面我们再换个视角看看。
根据业务规则——有这么一个数,当把它的最后一位(个位)挪到第一位的时候,得到的新数刚好是原来数的两倍——我们可以知道,这个数至少是两位以上的,并且可以人为的分为两个部分——个位部分和其他部分,可以用一个等式来表示这条业务规则想表达的意思:
2*(10X+Y) = Y*10 (n-1) + X
让我们继续化简这个等式:
20X+2Y = Y*10 (n-1) + X
↓
19X = Y*10 (n-1) – 2Y
↓
19X = Y (10 (n-1) -2)
最终我们得到了下面这个等式
X = Y (10 (n-1) -2)/19
在上面的等式中,Y表示个位上的数字,X表示其余的部分,n表示这个数的位数,例如:对于123这个数,Y=3,X=12,n=3。我们可以知道X和Y一定都是正整数,另外,我们还可以知道Y一定是一个0-9之间的数字,所以我们只要求得X的值,就很容易的可以知道我们要找的那个数了。
最后一个关键,就是n的取值,但其实这个值我们是可以控制的,我们可以尝试着给n赋一个值,然后把10 (n-1) 作为一个值来处理。如果在一个既定的范围内找不到我们需要的数,我们可以继续加大n的取值。这样在我们的等式中就只剩下X这一个未知数了。
最终我们得到下面的代码:
1 n = 1
2
3 #~ 计算10 的100次方内是否有我们要找的数
4
5 for i in 1..100
6
7 n =n * 10
8
9 for y in 1..9
10
11 #~ 如果找到了我们需要的数,就打印出“原数”和“新数”
12
13 if (y*(n-2))%19==0 then
14
15 puts "the original number is : " + (10*((y*(n-2))/19)+y).to_s
16
17 puts "the new number is : " + (2*(10*((y*(n-2))/19)+y)).to_s
18
19 end
20
21 end
22
23 end
24
执行这段代码后,我们获得了下面这些返回结果:
the original number is : 52631578947368421
the new number is : 105263157894736842
the original number is : 105263157894736842
the new number is : 210526315789473684
the original number is : 157894736842105263
the new number is : 315789473684210526
the original number is : 210526315789473684
the new number is : 421052631578947368
the original number is : 263157894736842105
the new number is : 526315789473684210
the original number is : 315789473684210526
the new number is : 631578947368421052
the original number is : 368421052631578947
the new number is : 736842105263157894
the original number is : 421052631578947368
the new number is : 842105263157894736
the original number is : 473684210526315789
the new number is : 947368421052631578
从中我们可以看到,除了标为红色的第一组以外,其他的数都是符合我们要求的。
前面写了这么大堆,当然目的还是想说明一下这两种方法的差别。
第一种方法是完全面向业务的分析和实现方法,代码并不算累赘,而且很容易通过阅读代码反向来了解业务的原始需求和业务规则;而后一种则是通过数学的方法进行分析和抽象之后得到的结果。相比较之下,相信大家不能看出两者之间执行效率上的差距——因为第一种方法的原理是从1开始逐个尝试。
我就不再计算圈复杂度或者执行效率之类的刻板数据了,让我们用一个更直观的方法来对比一下这两种算法的差别。
第一种方法是我最开始的做法,那段代码看似中规中矩,但是通过最后得到的结果我们可以看到,符合我们要求的最小的一个数也大于10的17次方,而使用我的方法在一台P4 3G + 1G 内存的机器上运行了一小时也不过才尝试到10的9次方,照此计算,至少要连续运行10多万年才能找到第一个符合要求的数字。
而第二种方法,则是得益于QQ群里一位昵称为“岚”的朋友的指点,使用这个方法,一秒中已经可以完成10的100次方以内的查找。
1秒 vs. 10万年!
这就是算法的力量!这就是知识的价值!
如果你还在用代码描述着业务,那么尝试一下第二种方法吧 ^_^