LeetCode一求素数算法优化的简单研究

前言


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=9999count最坏的结果也就进位增加一位数,不会增加两位数, 这里可以认为其循环次数大致介于为n~10*n,那么我们可以认为其时间复杂度为O(n)

4、总结

  • 每次在做类似简单的问题,我们大家都以为很简单就会跳过,草草了事。殊不知最简单的事情一旦研究起来里面还是有很多学问的,从很多小问题中日积月累,不断进步。当我们面对大问题时才会从有方法解决到有多种方法,最后到最优的方法。
  • 其中关于优化(3)的时间复杂度我不知道我这样的做法对不对,目前还没有经过数学推理,如果有大神能推导并且愿意分享,那将是我的荣幸!
  • 当然我并没有在空间复杂度上特别的强调优化, 关于求素数的算法肯定还有很多,日后有发现会及时更新研究,有大神路过能将其更好的方法告之,那再好不过了。
  • 虽然每次写博客,我都会先将代码实现然后多次测试,但是程序中可能还会有错误。所以很欢迎大家来指正,谢谢!

猜你喜欢

转载自blog.csdn.net/mynewclass/article/details/80272303