让我们来检查一下完全数的性质。这个
Math Overflow问题告诉我们两件非常有趣的事情:
- 完全数永远不是完全平方数。
- 完全数的形式为 (2k-1)×(2k-1)。
第二点非常有趣,因为它将我们的搜索范围减少到极小。在Java中,一个int是32位。而在这里,我们看到了幂和位位置之间的直接关系。由于这一点,我们只需要进行不到32次调用就能找到第5个完全数,而不是进行成百上千万次的isPerfectNumber调用。
因此,我们已经可以改变搜索范围,这就是你的主循环。
int count = 0;
for (int k = 1; count < 5; k++) {
int candidate = (1L << (k - 1)) * ((1L << k) - 1);
if (isPerfectNumber(candidate)) {
count++;
System.out.println(candidate);
}
}
这里是我们的重大突破。没有其他优化能够超越它:为什么要测试3300万个数字,当你只需要测试不到100个呢?
尽管我们获得了巨大的提升,但你的整个应用程序仍然可以改进,即你的方法isPerfectNumber(int)
。
目前,你仍然在测试过多的数字。完全数是所有真约数之和。因此,如果d
除以n
,n/d
也除以n
。你可以同时添加这两个约数。但美妙之处在于你可以停在sqrt(n)
,因为从数学上讲sqrt(n)*sqrt(n) = n
。所以你只需测试sqrt(n)
个约数,而不是测试n
个约数。
此外,这意味着你必须开始考虑边界情况。边界情况是1
和sqrt(n)
:
1
是一个边界情况,因为如果你将n
除以1
,你会得到n
,但你不会添加n
来检查n
是否为完全数。你只添加1
。因此我们可能会从1
开始求和,以避免太多的if
语句。
sqrt(n)
是边界情况,因为我们必须检查sqrt(n)
是否为整数,这很繁琐。但我参考的Math Overflow问题说没有完全数是完全平方数,所以这样可以简化我们的循环条件。
然后,如果在某个时刻sum
大于n
,我们可以停止。真约数之和大于n
表示n
是过剩的,因此不是完全数。这是一个小的改进,但实际上有很多候选者是过剩的。因此,如果保留该条件,你可能会节省一些循环次数。
最后,我们必须解决一个小问题:数字1作为候选项。1是第一个候选项,并且将通过我们所有的测试,因此我们必须对其进行特殊处理。我们将在方法开头添加该测试。
我们现在可以按以下方式编写该方法:
static boolean isPerfectNumber(int n) {
if (n < 2) return false;
int sum = 1;
int sqrt = (int)Math.sqrt(n);
if (sqrt * sqrt == n) {
return false;
}
for (int div = 2; div <= sqrt; div++) {
if (n % div == 0) {
sum += div + n / div;
if (sum > n) return false;
}
}
return n == sum;
}
这些优化技巧使得你甚至可以在一秒钟内找到第7个完美数,前提是你将代码改为使用long
而不是int
。并且你可以在30秒内找到第8个完美数。
所以这就是那个程序(在线测试)。我删除了注释,因为解释在上面。
public class Main {
public static void main(String[] args) {
int count = 0;
for (int k = 1; count < 8; k++) {
long candidate = (1L << (k - 1)) * ((1L << k) - 1);
if (isPerfectNumber(candidate)) {
count++;
System.out.println(candidate);
}
}
}
static boolean isPerfectNumber(long n) {
if (n < 2) return false;
long sum = 1;
long sqrt = (long)Math.sqrt(n);
for (long div = 2; div <= sqrt; div++) {
if (n % div == 0) {
sum += div + n / div;
if (sum > n) return false;
}
}
return n == sum;
}
}
以上程序的结果是前8个完全数的列表:
6
28
496
8128
33550336
8589869056
137438691328
2305843008139952128
如果您检查2k-1是否为质数,可以在搜索中找到更多的优化,正如Eran在他们的答案中所说,但考虑到我们对于long
类型的候选人少于100个,我不认为在这个程序中潜在地节省几毫秒是有用的,因为计算质数也可能很昂贵。如果要检查更大的完美质数,那么这样做是有意义的,但在这里吗?不是的:它增加了复杂性,而我试图保持这些优化方法相对简单和直截了当。
for(int i =1; i <= (a/2); i++){...}
,因为除数不能大于一半的数字。 - TuramarthisPerfectNumber
吗?一次是在if
之前,另一次是在if
中。结合 @Turamarth 的评论,这样可以加快速度。 - M. Deinum