取自然數中的素數幾種算法

ACM中的經典問題。網上看了大神的的各種算法,整理一下。
主要參考:http://blog.csdn.net/once_hnu/article/details/6302283
http://blog.csdn.net/liukehua123/article/details/5482854(這篇博文裏有幾處錯誤,可以看下面的評論)
http://blog.csdn.net/morewindows/article/details/7354571(提供利用位運算壓縮素數表)
感謝大神提供的思路。

一下代碼均爲java語言,且爲了簡潔,只貼出了核心部分,只要把核心部分放在一個類中,用main函數調用即可。

第一種,根據素數的定義,就是直接除以前面的數,看看是否能整除。

/*
n爲自然數的上界,下界默認爲1。

array[]爲存儲素數的數組,方便輸出。當數量級大的時候,均不輸出。

知識:如果一個數是合數,那麼一定至少有一個除了1以外的因子小於等於他的平方根。這點很重要,因爲只要判斷出一個因子,就可以確定他是合數了。這個結論大大減少了循環次數。

以上結論在後面的代碼均適用,就不在重複。
*/
    void tradition(int n,int array[]){
        for (int i=2;i<n;i++){//i爲需要判斷是否爲素數的自然數
            int j=2;//j爲除數
            for (;j<=Math.sqrt(i);j++){
                if(i%j==0){//取餘,等於0就說明j是i的一個因子
                    break;
                }
            }
            int mun=0;//用以循環賦值給array數組時的遞增量
            //j大於sqrt(i)說明j是自然循環出來的,而不是break出來的,就說明i沒有小於等於sqrt(i)的因子,所以i是素數
            if(j>Math.sqrt(i)){
                array[mun]=i;
                mun++;
            }
        }
    }

傳統算法容易理解,但是效率非常低,當n爲千萬時,耗時已經達到20s左右。

第二種,在第一種的基礎上,把偶數給排除,效率快了很多。

    void remove_even(int n,int array[]){
        array[0]=2;//2是偶數,但也是素數,所以特殊對待。
        //和第一種區別就在於i+=2這裏,也就是跳過了偶數
        for (int i=3;i<n;i+=2){
            int j=3;
            //只需要除以奇數,因爲偶數乘以任何自然數數都是偶數,不可能得到奇數。
            for (;j<=Math.sqrt(i);j+=2){
                if(i%j==0){
                    break;
                }
            }
            int mun=1;
            if(j>Math.sqrt(i)){
                array[mun]=i;
                mun++;
            }
        }
    }

較第一種優化了一點點,千萬時用時12s左右。

第三種,這裏的算法就牛逼了,體現差距的地方就在這裏了。

    void shaixuan(int n,int array[]){
        //定義一個布爾類型數組,他的下標就代表自然數
        boolean prime[]=new boolean[n];//默認值爲false
        prime[2]=true;//2特殊對待
        //這一步是爲了把奇數標爲true
        for (int i=3;i<n;i++){
            if ((i&1)==1)//奇數的位運算判斷方法
                prime[i]=true;//奇數就標爲true
        }
        //這一步的具體原理可以去看前面給出的兩篇博文。
        for (int i=3;i<=Math.sqrt(n);i++){//i就是數組的下標,也就是要判斷的自然數
            if (prime[i]){//true就說明是奇數,需要判斷是否爲素數
                for(int j=i*i;j<n;j+=2*i)//後面具體解釋
                    prime[j]=false;//把i的倍數都標爲false
            }
        }
        //同上,爲了賦值給array 
        int mun=0;
        for(int i=2;i<n;i++){
            if (prime[i]){
                array[mun]=i;
                mun++;
            }
        }   
    }

這種方法看起來更長了,代碼也多了,但是效率比上面的快了不是一點兩點。千萬的速度是150ms左右。自己體會下效率快了多少。
引用上面博文的解釋:
一個簡單的篩素數的過程:n=30。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
第 1 步過後2 4 … 28 30這15個單元被標成false,其餘爲true。
第 2 步開始:
i=3; 由於prime[3]=true, 把prime[6], [9], [12], [15], [18], [21], [24], [27], [30]標爲false.
i=4; 由於prime[4]=false,不在繼續篩法步驟。
i=5; 由於prime[5]=true, 把prime[10],[15],[20],[25],[30]標爲false.
i=6>sqrt(30)算法結束。
第 3 步把prime[]值爲true的下標輸出來:
for(i=2; i<=30; i++)
if(prime[i]) printf(“%d “,i);
結果是 2 3 5 7 11 13 17 19 23 29

自己理解一下,以3爲例,就是把因子裏包括3的合數都給標記爲false了。

for(int j=i*i;j<n;j+=2*i)

這一句的判定是這樣的:還是以3爲例,當i=3時,那以3爲因子的合數就是6,9,12,15….,也就是3*2,3*3,3*4,3*5….,也就是3+3,3+2*3,3+3*3,3+4*3….這裏再省一步,因爲6,12都是偶數,在之前已經標記爲false了,所以可以直接跳過,就變成3+2*3,3+4*3…..

int j=i*i,以5爲例:5*2,5*3,5*4,5*5,5*6….,乘以偶數的就不用考慮了。乘奇數的,小於5的奇數,那肯定也被這個奇數或者這個奇數的因子去掉了。這樣就避免重複了。所以起步就從i*i開始了。

j+=2*i就是上面解釋的。

第四種,在第三種的基礎上再優化一點。就是前面給出的博文裏提到的博士獨創的方法。

/*
這裏的思路是在預設布爾數組的時候,就已經考慮把偶數的排除掉了,
也就是認爲裏面只有奇數。存儲的數據是這樣的:
23579111315171921....
且與數組下標對應:
012345678910....
也就是下意識要認爲其中的prime[0]=2;prime[1]=3....
這樣的做法是省去了到偶數哪一步的判斷。
*/
    void shaixuan(int n, int array[]){
        //因爲只有奇數,所以對n的自然數只有一半的奇數,當然這裏很可能會丟掉最尾巴的那個數,這裏就不做那麼細的考慮了。
        boolean prime[]=new boolean[n>>1];//除以2的位運算方式

        for(int i=1;i<=Math.sqrt(n>>1);i++){
            //這一步是因爲布爾數組默認值爲false,爲了簡便,這裏就認爲false是要的,true是不要的。
            if(prime[i]==false){
                for(int j=2*i*i+2*i;j<n>>1;j+=2*i+1){//這一步判斷比較繞,後面解釋。
                    prime[j]=true;//思路和上面一樣,把倍數賦值爲true
                }
            }
        }

        int mun=1;
        array[0]=2;
        for(int i=1;i<n>>1;i++){
            if (prime[i]==false){//false纔是素數
                array[mun]=2*i+1;//i是下標,2*i+1纔是實際的數值
                mun++;
            }
        }
    }

千萬的速度是110ms左右,是不是又快了一點。

其實思路還是第三種的思路,只是這裏對偶數的處理更加巧妙了。
for(int j=2*i*i+2*i;j>1;j+=2*i+1)其實判斷的本質和原來的判斷是一樣的,只是這時候不能認爲下標就是數字,還需要把下標轉化成實際的數值。以3爲例,3和3的倍數:
數組的下標:1,4, 7, 10…
實際的數值:3,9,15,21….
思路還是原來的,想把3的倍數賦值爲true(這裏true和false是反過來的)。那就是把9 ,15,21…賦值爲true,但是並不是直接把prime[9],prime[15],prime[21]…賦值爲true,還需要找到這些數值對應的下標,也就是prime[4],prime[7],prime[10]…賦值爲true。

假設我不知道9對應的下標是多少,設爲x。原來的int j =i*i,那這裏就是:
(2*i+1)(2*i+1),但是這只是值,不是下標,還要解(2*i+1)(2*i+1)=(2*x+1),這裏把x解出來,纔是下標,x=2*i*i+2*i。把i=1帶進去驗證一下,x=4,i=1對應的值是3,x=4對應的值就是9,沒錯吧。所以int j=2*i*i+2*i就是j的初始值。

對於j+=2*i+1;這個看規律也能看出來。非要計算的話,設要求的式子爲j+=x;
原來的是:j+=2*i。對應這裏的i則要變化爲2*(2*i+1)。意思就是兩個緊鄰的倍數之間的差值爲2*(2*i+1)。
其實對應的式子是:prime[j+x]-prime[j]=2*prime[i]
對應的數值是2*(j+x)+1-(2*j+1)=2*(2*i+1),解得x=2*i+1。

第五種,利用位操作壓縮素數表,並且效率也提高了。

    void compress_save(int n,int array[]){
        for(int i=1;i<=Math.sqrt(n>>1);i++){
            if((array[i/32]>>(i%32)&1)==0){
                for(int j=2*i*i+2*i; j<n>>1; j+=2*i+1){
                    array[j/32]|=(1<<j%32);
                }
            }
        }
    }

千萬的速度是55ms左右,這個效率已經夠苛刻了吧。

解釋一下原理:
其實實際原理和第四種一模一樣,只是存儲數據的方式變了,也就是所謂的數據結構吧。第四種採用布爾數組存儲奇數,對小於n的自然數,那就是需要n/2長度的布爾數組,布爾數組當做byte數組處理(網上資料,不敢保證一定正確)。也就是n/2個字節。int數據在內存中爲32位二進制數,如果我把每一位數都對應爲一個自然數,就是一個int數據就可以存儲32個整數(實際有一點點變化,後面講),因爲數組在內存中是連續的,所以可以把數組看成是一個整數。例如數組長度爲2,那麼就是連續的兩個int數據在一起,如果第一int數據的第一位標記爲0,那最後一位爲31,緊接着的下一個int數據的第一位是不是就可以看成是第32位,最後一位看成是63位?這樣就是存儲了64位數了。一次類推,數組長度變成n,可以存儲的數就變成了32*n。這樣就大大節省了空間,特別是素數問題裏都是要求超大型數據。

我認爲的數據存儲依然是這樣的:
2,3,5,7,9,11,13,15,17,19,21….
且與數組下標對應:
0,1,2,3,4, 5, 6, 7, 8, 9, 10….
實際操作其實就是把數組裏的一個數看成32個數,也就是array[0]的第一位爲2,第二位爲3,仍然滿足2*i+1的關係(除了第一位)。然後操作就是之前介紹的。

解釋下那幾句位運算:
(array[i/32]>>(i%32)&1)==0
i是從0開始的,當i=31時,對應的剛好是aarray[0]的最後一位,i=32時,對應的是array[1]的第一位,i/32=1,已經到array[1]了,所以這裏不是除以31而是32。
這句話的作用是:例如i=5,先將array[5/32]=array[0]這個數右移5%32=5位,再與1進行與運算,判斷這一位是否爲0(與1進行與運算是位運算裏用的最多的一種方式之一)。因爲默認的所有位都爲0,判斷爲合數的爲就會置1,這一步判斷爲0的數就是素數。就相當於第四種中的prime[i]==false,功能一模一樣看懂第四種,這裏就好理解了。

int j=2*i*i+2*i; j<n>>1; j+=2*i+1和第四種一樣,沒變。

array[j/32] |= (1<<j%32);功能與第四種的prime[j]=true;一模一樣,只不過這裏對某一位賦值,比較特殊而已。具體原理可以看給的第三個鏈接。

還得解釋一下如何輸出素數,這裏不像前面,都把它存在一個數組裏了,直接輸出數組就完事了。需要稍微轉一下彎,方式如下:

    System.out.print(2+" ");//2總是特殊對待
    for(int i=1; i<n>>1;i++)
    //判斷第i位是不是0,是0說明這一位存儲的是素數,而i對應的數值是2*i+1,所以輸出2*i+1即可
        if(((array[i/32]>>i%32)&1)==0)
            System.out.print(2*i+1+" "); 

下面是我所有代碼採用的統一的main函數寫法,修改n的值就可以修改數量級,當然定義的類和函數的名稱不一樣,用到時記得修改。

public class main {
    public static void main(String[] args) {
        filter s1=new filter();
        int n=10000000;
        int array[] = new int[n>>1];
        long startTime = System.currentTimeMillis();//獲取當前時間
        s1.shaixuan(n, array);
//      for(int i=0; i<array.length;i++)
//          System.out.print(array[i]+" ");
        long endTime = System.currentTimeMillis();//獲取當前時間
        System.out.println("程序運行時間:"+(endTime-startTime)+"ms");
    }
}

最後再扯一下array數組大小的問題。在main函數裏,定義是這樣的:

int array[] = new int[n];

五種方法裏,大小的取值依次爲:
①n,②n,③n/2,④n/2,⑤n/40+1
主要分析第5種,理論上n/64是剛好的(n/2再/32位),但是這裏不能剛好。

        for(int i=1;i<=Math.sqrt(n>>1);i++){
            if((array[i/32]>>(i%32)&1)==0){
                for(int j=2*i*i+2*i; j<n>>1; j+=2*i+1){
                    array[j/32]|=(1<<j%32);

舉例:n=100時,sqrt(n>>1)=7,n/64=1。上面代碼,當i=6時,(array[i/32]>>(i%32)&1)==0,此時j=2*6*6+2*6=84, array[84/32]=array[2], 然而array長度只爲1,所以會報錯。
sqrt(n>>1)是不能優化了(不會),那就只能適當的放大array的長度,n/40+1是我隨便測試出來的,並不是完全吻合,懶得去追求那麼完美了。+1主要是照顧n<40的時候。

ps:以上幾種算法均不是自創,借鑑前面提到的三篇博文的思路,自己一點點寫的。難免有錯誤的地方,還請不吝指教。如果有其他更好的方法,更請賜教。

(爲什麼有部分代碼的註釋不會變成紅色了?不懂什麼情況)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章