前言
1.求素数对于大多数人都比较简单,谁不知道啊。一个数n只存在1与自身能整除的数就为素数,并且编写代码也相对容易,很快就能写出。
2. 但是如果我现在要求优化求素数的算法呢?你能做到几步优化?从时间上优化,从空间上优化都能实现吗?
1、常规的算法及实现
(1)简单算法描述:
第(1)步:在
2~n-1
中取数循环除以n
,如果能整除就返回false
退出,否则继续循环直至n-1
,最后返回true
第(2)步:对返回true
的数加入集合或者数组,即为素数集合
(2)Java代码实现(分模块化):
/**
* 1.判断一个数是否是素数很简单,只需要求输入的n在2~n-1数中都找不到能被整除的数,那么n就是素数
* 也就是只能被1与n(本身)整除的数就是素数
* @return true/false
*/
public static boolean isPrime(int n) {
for (int i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
/**
* 2.获得所有的素数集合
* @param n
* @return
*/
public static List<Integer> getPrimeList(int n) {
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (isPrime(i)) {
primes.add(i);
}
}
return primes;
}
(3)Java代码实现(不分模块化):
public static List<Integer> getPrimeList(int n) {
List<Integer> primes = new ArrayList<>();
boolean isPrime = true;
for (int i = 2; i <= n; i++) {
for (int j = 2; j < i; j++) {
if (i % j == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
System.out.println(i);
}
isPrime = true;//必须重置标识位
}
return primes;
}
2、优化算法及实现
对于常规的算法我们做以下几点的考虑:
(1)有必要从2
循环到n-1
吗? 答案是否定的,我们只需要循环到Math.sqrt(n)
即n
的开平方即可,这是因为:
数M被小于它的数N整除,那么M/N^2(M>=N^2)也会被整除比如n=16, 我们从2,3,…,15中找素数,16/2能整除,16/4自然也能整除即可,当输入的n很大时,减少的可是数量级循环次数,也节约了大量时间
但是只能用在Java实现(2)
中:
public static boolean isPrime1(int n) {
for (int i = 2; i < Math.sqrt(n); i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
(2)有必要从2
一直循环到n
吗? 其实是没必要的
当我们已经从
2~n
中找到一个素数m
后,在2~n
中所有m
的倍数全部都不需要再进行判断,这是因为任何一个合数都可以分解为若干个质数相乘,自然都是倍数关系。虽然说
Math.sqrt(n)
大大减少了判断一个数是否是质数的时间复杂度,但是外层循环for (int i = 2; i <= n;
循环次数仍然不会变,当
i++)n
很大时,循环次数仍然很大,循环了(n-2)*(sqart(n)-2)
次,时间复杂度为O(n^(3/2))
,为了减少外层循环我们可以将已经是质数的倍数全部在区间[2,n]
中去除。
(3)有必要运用“ 从2到本身或者从2到本身开平方判断是否整除 ”这一思想来判断是否是质数吗?答案是否定的
- 试想下,虽然我们已经
n^2
减少到n^(3/2)
,但是当n
足够大时,比如n=100*10000
,内层循环也循环了1000-2
次,外层就更不用说。如果结合优化(2)
,将外层循环可以次数降低,内层循环仍然不能降低(注意:先根据内层循环判断为质数才会删除该质数的倍数,所以内层循环不能被降低) - 那么我们换个思路,在数据序列
[2,n]
中将2的倍数去除只留下2,将3的倍数去除只留下3…….这样最后得到的数列就为素数集合,没必要再进行内层循环判断是否为素数再加到素数集合中。具体的如下过程:
开始:
数据序列: 【2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20】
删除2的倍数:【2,3,5,7,9,11,13,15,17,19】
删除3的倍数:【2,3,5,7,13,17,19】
完成
这样每一次删除可以大大减少下一次的循环次数,最坏的结果输入的数据序列是1-n
序列,如果是等比序列从小到大排列呢,一步就可以得到结果。
- 用Java代码实现要求对集合的操作较为熟练:
public static List<Integer> isPrime4(List<Integer> primes) {
int multiple = 2;//倍数
for (Integer natural : primes) {
multiple = 2;
//终止条件:倍数已经超出n则停止
while(natural*multiple <= primes.get(primes.size()-1)) {
int delNum = natural*multiple; //要删的数
//利用Java8 stream 过滤删除,也可以使用primes.remove()
primes = primes.stream().filter(integer -> integer != delNum).collect(Collectors.toList());
multiple++;
}
}
return primes;
}
3、优化算法的比较
在程序中我们加入统计循环次数count
计数器之后,统计各个方法循环的次数:
其中优化(3)需要输入随机数据序列时(Random rand = new Random(); rand.nextInt(n-2)+2
)运行
n的输入 | 常规算法 | 优化(1)使用开平方 | 优化(3)使用筛选-删除法 |
---|---|---|---|
10 | 24 | 17 | 10 |
100 | 1232 | 335 | 241 |
1000 | 79021 | 6287 | 2023 |
10000 | 5785222 | 127526 | 49947 |
100000 | … | … | … |
显而易见,优化(3)的方法是最好的,其实后面经过我的多次试验,我发现每次优化(3)当我输入n=99,n=999,n=9999
,count
最坏的结果也就进位增加一位数,不会增加两位数, 这里可以认为其循环次数大致介于为n~10*n
,那么我们可以认为其时间复杂度为O(n)
4、总结
- 每次在做类似简单的问题,我们大家都以为很简单就会跳过,草草了事。殊不知最简单的事情一旦研究起来里面还是有很多学问的,从很多小问题中日积月累,不断进步。当我们面对大问题时才会从有方法解决到有多种方法,最后到最优的方法。
- 其中关于优化(3)的时间复杂度我不知道我这样的做法对不对,目前还没有经过数学推理,如果有大神能推导并且愿意分享,那将是我的荣幸!
- 当然我并没有在空间复杂度上特别的强调优化, 关于求素数的算法肯定还有很多,日后有发现会及时更新研究,有大神路过能将其更好的方法告之,那再好不过了。
- 虽然每次写博客,我都会先将代码实现然后多次测试,但是程序中可能还会有错误。所以很欢迎大家来指正,谢谢!