Java实现快速查找某个范围内的所有素数

Java实现快速查找某个范围内的所有素数

  • 前言
  • 定义法
  • 筛选法
  • 筛选优化法
  • 后记

前言

素数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数。定义非常简单,但是它却难以定量化,研究起来非常复杂,有兴趣的可以买本研究素数的书看看。前几天去B站,看到有关这方面的介绍,给个传送门:素数。
我这里主要是介绍几种查找素数的方法,研究这些算法优化的思路。

定义法

我们一般判断素数都是利用求余的思想,因此查找素数也可以采用这种思想,逐个判断。代码如下:

public static void primes(int n){
    if(n < 0){
        throw new IllegalArgumentException("N must be a non negative integer.");
    }
    if(n <= 1){
        return new int[0];
    }
    int primeNums = (n & 1) == 1 ? (n >>> 1) + 1 : n >>> 1;
    int[] primeArray = new int[primeNums];
    int primeCount = 0;
    outer:
    for(int i = 2; i <= n; i++){
        for(int j = 2, limit = (int) Math.sqrt(i); j <= limit; j++){
            if(i % j == 0){
                continue outer;
            }
        }
        primeArray[primeCount++] = i;
    }
    return Arrays.copyOf(primeArray, primeCount );
}

因为一定范围的素数数量难以精确估量,因此我选择最保守的方式,来估计它的数量。n为奇数,数量就定为n/2+1,n为偶数,数量则为n/2。数学上其实有一些估计函数,可以相对精确估计其数量,具体参考百度百科:判断素数
如果大家对判断素数比较了解,应该会对sqrt(n)这个值比较熟悉。该值主要是为了减少重复求余判断,提升效率。网上找了个通俗解释,放个传送门:为什么判断一个数是否为素数时只需开平方根就行了?
这个算法比较简单,效率也相对较低,假设我们输入n=100,我们可能判断了2、4、6、8、10…是否为素数,也判断了3、6、9、12、15…是否为素数,等等。仔细观察,这些数都是素数的倍数。如果我们判断了某个数为素数,再将它的倍数全部设置为合数,那我们判断的数量会大大降低,查找素数的效率也会得到极大提高。就像选择排序相对于冒泡排序,也是减少了大量的swap操作,因此效率也得到了相应提升。

筛选法

前面我们说到定义法找素数的局限性时,提到将素数的倍数设置为合数,提升后面查找素数的效率。这种方法最早被古希腊的埃拉托斯特尼提出。具体代码实现如下:

public static int[] primes(int n){
    if(n < 0){
        throw new IllegalArgumentException("N must be a non negative integer.");
    }
    if(n <= 1){
        return new int[0];
    }
    boolean[] primes = new boolean[n + 1]; // 加一实现下标与真实数值对应,boolean默认为false
    /* 将下标为奇数的置为true,下标为偶数的默认为false。*/
    for(int i = 1; i <= n; i++){
        if((i & 1) == 1){
            primes[i] = true;
        }
    }
    for(int k = 3, limit = (int) Math.sqrt(n); k <= limit; k += 2){
        /*将素数倍数下标处的值全部置false*/
        if(isPrime(k)){
            for(int i = k * k; i <= n; i += k){
                primes[i] = false;
            }
        }
    }
    int primeNums = 0;
    /*获取精确的素数数量,以免开辟过大的数组造成空间不足的情况。*/
    for(boolean isPrime : primes){
        if(isPrime){
            primeNums++;
        }
    }
    int[] primeArray = new int[primeNums];
    primeArray[0] = 2;
    int count = 1;
    for(int i = 3; i <= n; i++){
        if(primes[i]){
            primeArray[count++] = i;
        }
    } 
    return primeArray;
}

该算法首先初始化n+1(为了让下标和数值对应)长度、boolean类型的prime数组,用于存储是否为素数的信息。然后将奇数下标的值全部设置为true,即认定奇数为伪素数。接着在3:2:sqrt(n)范围内,选定素数,这个判断素数的函数isPrime参见:Java实现素数的判断,然后将该素数的倍数置合,即设置它们为非素数。最后遍历prime数组,将数值为true的下标值认定为素数,并将偶数2也添加入素数数组。
这里我们需要注意的是将素数倍数值置合的代码,如果k是素数,那么我就将k * k : k : n(Matlab写法)的值全部认定为合数,如果你们看过网上其他人的代码,几乎清一色的是k + k : k : n,他们可能认为从k的两倍开始,将所有k的倍数全部取到,但是却忽略了一些东西,打个比方,如果k = 7,我们将14、21、28、35…等设置为合数,这本身并没问题,但是14是2的倍数,在k=2时,我们就已经被置为合数了,21是3的倍数,在k=3的时候也已经被置为合数了,以此类推,我们其实重复操作了很多数据。因此我们选择从k2这个最新倍数开始进行倍数置合操作,它的原理和前面的sqrt(n)一模一样。虽然这个问题不起眼,但我们还是需要注意,写算法时,一定要仔细考虑清楚算法的个个细节。

筛选优化法

前面介绍的筛选法,其实效率已经非常高了,只是有一个问题需要解决,就是它的prime数组开辟的过大,假设n等于Integer.MAX_VALUE,那么该算法直接抛出NegativeArraySizeException,就算比最大值小点,也可能出现OutOfMemoryError,因此我们需要减少该数组的大小。不知大家注意了没,前面筛选法我们在处理前,先将偶数全部置合数。就是说明除了2的偶数全部都是合数,绝不可能是素数,因此我们可以只开辟一半的数组,全部保存奇数。然后再进行后面的筛选操作。代码如下:

public static int[] primes(int n){
    if(n < 0){
        throw new IllegalArgumentException("N must be a non negative integer.");
    }
    if(n <= 1){
        return new int[0];
    }
    int len = ((n & 1) == 1) ? (n >> 1) + 1 : n >> 1;
    boolean[] p = new boolean[len + 1];
    for(int k = 3, limit = (int)Math.sqrt(n); k <= limit; k += 2){
        if(!p[(k + 1) >> 1]){
            for(int j = (k * k + 1) >> 1 ; j <= len; j += k){
                p[j] = true;
            }
        }
    }
    int primeNums = 0;
    /*获取精确的素数数量,以免开辟过大的数组造成空间不足的情况。*/
    for(int i = 1; i <= len; i++){
        if(!p[i]){
            primeNums++;
        }
    }
    int[] primeArray = new int[primeNums];
    primeArray[0] = 2;
    int count = 1;
    for(int i = 2; i <= len; i++){
        if(!p[i]){
            primeArray[count++] = i * 2 - 1;
        }
    }
    return Arrays.copyOf(primeArray, count);
}

首先我们的数据全是奇数位,因此实际数据都需要按照2 n - 1进行转换,如下所示
实际数据:1 2 3 4 5 6 7 8 9 10
奇数数据:1 3 5 7 9 11 13 15 17 19
代码先是按奇数数据的3 :2:sqrt(n),取所有奇数,然后判断此时实际数据(k + 1)/ 2的倍数是否已经被置合。如果没有,然后就对其倍数进行置合,在前面筛选法我们知道范围是k2: k : n,此时我们将其转换到实际数据中来,因为奇数数据中的k2转换为实际数据的公式是(2 * p - 1)= k2,因此实际数据的初始点为(k * k + 1)/2,但是我们的步长k却没有转换呢,因为实际数据和奇数数据是线性关系,步长的变换是相同。打个比方,当奇数数据为3时,它的倍数就是9,此时我们按照前面的转换,实际数据的倍数位置就是9对应的5,如果奇数值加个步长3,则转到15,此时我们的实际数据,则按照(2 * n - 1)换算为8,此时实际数据8和5的步长间隔也是3,因此步长不变,截止值从数值n变成n的一半,综上可知奇数数据的范围k2: k : n转换到实际数据时,范围变成了(k2 + 1) / 2 : k : len,这个len就是小于等于n奇数的长。
最后将实际数据中界定为素数的数据全部按照2* i - 1转换为奇数数据,再另外加上2这个特殊的素数,即可。
由此该算法就达到了减少筛选所需的空间的目的,空间效率得到了提升。这一段算法主要参考自Matlab2014a的primes函数,网上好像没有在线的Matlab代码资源,只有安装Matlab软件才能看到。

后记

今天看了一些关于素数的理论知识,还真的挺有意思的,现在想想数学还真是个好东西。最后说一句,很多优秀的算法,Matlab都有相应的实现,并且代码价值非常之高,虽然它的矩阵化操作实现起来效率很低(没有底层支持),但是它的算法思想很值得研究。

你可能感兴趣的:(Java学习)