最近看《java核心技術》看到集合章節,在最後位集(BitSet)部分給出了一個示例程序,使用了埃拉托色尼篩選法(Eratosthenes Sieve)求自然數2~n範圍的所有素數
代碼如下:
import java.util.BitSet;
public class Sieve {
public static void main(String[] args) {
int n = 2000000;
long start = System.currentTimeMillis();
BitSet bitSet = new BitSet(n+1); //開闢2000001位的位集空間,從0位開始
int count = 0; //記錄素數個數
int i;
for(i=2; i<=n; i++) {
bitSet.set(i); //將所有位置“開”狀態(置1)
}
i = 2;
while(i*i <= n) { //從i=2開始遍歷小於等於根號n所有整數
if(bitSet.get(i)) { //如果是素數
count++; //count增加
int k = 2 * i; //k爲素數i的倍數,所以爲合數,初始化爲2倍
while(k <= n) {
bitSet.clear(k); //清除位集中的第k位,置爲“關”狀態(置0)
k += i; //k倍增
}
}
i++; //i遞增,最終增至大於根號n的最小整數
}
while(i <= n) {
if(bitSet.get(i)) {
count++;
}
i++;
}
long end = System.currentTimeMillis();
System.out.println(count + " primes");
System.out.println((end - start) + " milliseconds");
}
}
這個程序給定的n是2000000,結果並沒有打印出所有素數,只是得出素數的個數爲148933。
書中稍微提及了算法的基本操作:遍歷一個擁有200萬個位的位集,首先將所有的位都置爲“開”狀態,然後將已知素數的倍數所對應的位都置爲“關”狀態,經過這個操作保留下來的位所對應的就是素數。
作爲一個碼渣,在看過這個描述和代碼之後,基本處於黑人問號臉狀態。於是本着刨(zuan)根(niu)問(jiao)底(jian)的精神,決定仔細研究研究,好在最後終於大概明白了算法的基本思想,記錄於此以備忘。
首先介紹兩個很簡單的預備點:
(1)一個合數 n 必有一個因子 i ≤ √n(根號n)
對於在程序中如何判斷自然數 n(n > 2)是否爲素數,自然而然想到的是枚舉法:對 n 進行除法遍歷運算,除數爲小於 n 大於等於2的自然數,即用 n 依次除以 2、3、4、…、n-1,如果每一次除法都不能整除(有餘數),則說明 n 爲素數;否則 n 爲合數,能整除的除數即爲 n 的一個因子。
通過以上步驟可以看出,在程序中要判斷自然數 n(n > 2)是否爲素數,需要進行 n-2 次除法運算。對此可以進行優化,使除法運算次數減少。
現假設 n 爲合數,有i_1 * i_2 = n,則 i_1 和 i_2 爲 n 的兩個因子,進而有i_1 * i_2 = √n * √n,則必有i_1 ≤ √n或者i_2 ≤ √n,即合數 n 必有一個因子小於等於√n。所以只要能夠在√n 範圍內找到 n 的一個因子,就可以判斷出 n 爲合數而非素數。據此進行優化即可將判斷 n 是否爲素數的除法運算次數減少至 [√n] -1 次([√n] 爲小於√n的最大整數),只需用 n 依次除以2、3、4、… [√n] ,判斷是否能夠整除即可。
(2)一個合數必能分解爲全部是素數的因子
假設合數 n 可以分解出非素數因子(合數因子),那麼這個非素數因子當然還可以繼續分解,直到所有因子全部爲素數。
即 n 可以表示爲全部是素數因子的乘積:n = i_1 * i_2 * i_3 * … * i_j ,其中i_1、i_2、i_3、… 、i_j全部爲素數。
綜合以上兩點可以得出:一個合數 n 必定有一個素數因子 i ≤ √n
========以下進入本算法主題==============
對於求自然數2~n 範圍的所有素數,即等於篩掉區間[2, n]的所有合數。由上述結論一個合數 n 必定有一個素數因子 i ≤ √n ,可得出:區間[2, n]的任一合數m,必有一個素數因子 i_m ≤ √n。
將這個小於等於 √n 的素數因子 i_m 作爲切入點,使 i_m 成倍增加(2*i_m, 3*i_m, …),並限定i_m倍增的結果小於等於n,則必定能得到對應的在區間[2, n]的合數m。由於素數因子i_m ≤ √n,只要遍歷2~[√n]中的素數,使這些素數倍增,就能得到區間[2, n]中的所有合數。篩去這些合數,剩下的就是自然數2~n範圍的所有素數了。
以上就是埃拉托色尼篩選法(Eratosthenes Sieve)的基本思想。
可能有人會疑惑:又該如何遍歷2~[√n]中的素數呢,因爲這需要先找出來2~[√n]範圍內的素數,之後才能進行遍歷。
仔細想一想,其實這是個遞歸問題。因爲找出2~[√n]範圍內的素數,這個問題與初始問題——找出2~n範圍的素數,是一致的,依然可以使用本算法,只不過將n換成了[√n],查找區間變小了,如此遞歸下去,最終問題將演變成找出2~2或2~3範圍的素數,答案就顯而易見了。
以上可以說是屬於逆向分析,查找區間逐漸縮小以簡化問題;以下進行正向分析,從第一個素數2開始,逐漸擴大區間。
以下進行正向分析,從第一個素數2開始推導,逐漸擴大查找區間,以驗證本算法
1、對於第一個素數2 = √4,區間[2, 4]範圍的所有合數必有素數因子小於等於2。將2倍增,增大一倍即得到以素數2爲因子的合數4,然後篩除掉合數4,則在2~4範圍只剩下了素數2、3。
2、對於在第1步中得到的2~4範圍的素數2、3,有 3 = √9,區間[2, 9]範圍的所有合數必有素數因子小於等於3。對素數2、3進行倍增:將素數2分別增大1倍、2倍、3倍可得到合數4、6、8,於是篩除4、6、8;將素數3分別增大1倍、2倍可得到合數6、9,於是篩除6、9。經過對第一步中得到的2~4範圍剩餘的素數進行遍歷倍增並篩除其結果的操作,可知到2~9範圍剩下的全部是素數:2、3、5、7。
3、同樣按照本算法,遍歷倍增第2步中得到的2~9範圍的所有素數2、3、5、7,並篩除掉相應的合數,可將區間擴大爲[2, 49],得到2~49範圍的所有素數。因此,不斷按照這種步驟運算,就可以得到任意範圍的所有素數。
以上即爲從第一個素數2開始,不斷擴大素數區間的正向推導過程。
=========以下結合java代碼來分析本算法的實現過程========
由於示例java代碼是在集合章節,所以使用了位集(BitSet)。2~n爲查找範圍,這裏n=2000000。在程序一開始構造了一個2000001位的位集,每一位的索引即對應一個數:第0位對應0,第1位對應1,…,第2000000位對應2000000。位集的所有位都被初始化爲“開”狀態(置1),“開”狀態則表示當前位對應的數爲素數,反之,“關”狀態則說明當前位對應的數位合數。變量count用來記錄素數的個數。
程序中變量 i 就表示2~n 範圍的合數必存在的小於等於√n 的因子,初始化爲第一個素數2。因此,在第一個while循環的判定條件中限定 i * i <= n,並在循環中遞增。在第一個while循環內部,如果bitSet.get(i) 得到第 i 位爲“開”狀態,則count 增加1。變量 k 即爲對因子 i 進行倍增所得到的合數,初始時先乘以2增加一倍,然後在內部的while循環中對 k 不斷倍增並判斷得到的合數是否小於等於n,符合條件則清除在位集中相應的第k位,置爲“關”的狀態(置0),這就相當於把2~ i * i 範圍的合數全部篩除掉了,剩下的則全部爲素數,如此就保證了 i 在遞增的過程中,只有素數索引位的bitSet.get(i) 返回的是“開”狀態結果。
在 i 遞增到 i * i > n 的時候,第一個while循環退出,整個2~n範圍的篩選過程結束,在位集中只有素數索引位保留爲“開”狀態,合數索引位全部被置爲了“關”狀態。最後再遍歷剩下的 i ~ n 範圍的素數,就得到了 2~n 範圍的素數個數結果,有需要的話也可以把相應的素數保存或者打印。
以上就是對 埃拉托色尼篩選法(Eratosthenes Sieve)求素數算法思想的簡單介紹,至於算法的複雜度問題,本人並不太擅長,就不在此獻醜了。
(完)
本篇是自己對此算法的一個理解和記錄,同時也是第一次寫算法分析的文章,肯定有諸多問題未能察覺,誠懇接受各種指導和批評建議,並在以後努力改善。