1. 程式人生 > >Java實現快速查詢某個範圍內的所有素數

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都有相應的實現,並且程式碼價值非常之高,雖然它的矩陣化操作實現起來效率很低(沒有底層支援),但是它的演算法思想很值得研究。