埃拉托色尼的篩子
生成素數有很多方法,本文介紹的算法是一種高效的篩選算法 ---埃拉托色尼篩選法。
比如,要產生[2,n] 范圍內的所有素數,步驟如下:
1、構造一個2,3,4,5,...n 的候選數序列 A 。
2、不斷的去除(篩掉)序列A中的非素數。
①去掉2的倍數 。
②再去掉3的倍數。
③去掉4的倍數(不需要,因為在第一步已經被去掉了) 去掉5的倍數。
④去掉6的倍數
⑤去掉7 的倍數
... ... 一直到不能再去除為止。
3、經過反復的篩選去除后,序列A就只剩下了[2,n]內的素數
例子:產生[2,25]范圍的素數序列
代碼實現
/* 使用一個bool 數組。數組的下標代表待篩選的數,而用數組元素的值代表 數的存在與否。 如 A[p] = true 表示數p 存在,沒有被去除。則p為素數 */ void select_prime(int n) { if (n < 2) return ; const bool exist = true; const bool not_exist = false; bool*A = new bool[n + 1]; //會浪費 數組的第一 和第二個元素空間.因為 0 和 1 既不是素數,也不是合數。 memset(A, exist, sizeof(bool)*(n + 1)); //初始化為exist for (int p = 2; p*p <=n; p++) { if (A[p] == exist) // { // int j = p*p; // while (j <= n) // { // A[j] = not_exist; // j += p; // } // } // } //至此 ,使得A[p]為true 的p都是素數。這里不再具體操作,取決於你自己的實現。如打印,或者轉存 delete[] A; }
代碼解讀
整體代碼,估計就是for循環那段代碼可能不好理解。我們先分析for內部的代碼。
if (A[p] == exist) //如果p是素數 { int j = p*p; //則從p*p開始 去除 while (j <= n) { A[j] = not_exist; //標記為 去除狀態 j += p; //遞增到下一個倍數 } }
也就是說,對於一個數p,會依次去除 p*p , p(p+1) , p(p+2) .... p(p+k) 【p(p+k)<=n】
前面不是說要去除 p 的所有倍數的嗎?那 2p ,3p 4p ...p(p-1)怎么不去除呢?
他們已經被去除了。因為當前我們要消去 p 的倍數,那么,之前一定去除了 p-1 , p-2 ,p-3 ... 4 , 3 , 2 的這些數 的倍數,So , p(p-1) , p(p-2) .... p3 p2 顯然在之前的操作中被去除。
也正是因為這個原因,最外面的for循環,p 的值從 2 到 ,而不必從 2 到 n。為了避免使用sqrt函數帶來消耗,我使用了乘法,是同樣的效果。
即便通過技巧減少不必要的去除操作,上面的代碼依然存在重復去除的可能。比如數字35,在去除5的倍數時被標記為 去除 ,在去除7的倍數時,也會再次被標記 。當然這不影響結果的正確性。但是如果要統計素數的個數,就需要小小的修改一下代碼了。
統計素數的個數
int countPrime(int n) { if (n < 2) return 0; const bool exist = true; const bool not_exist = false; unsigned count = n - 1; //假設全部是素數 bool*A = new bool[n + 1]; memset(A, exist, sizeof(bool)*(n + 1)); for (int p = 2; p*p <= n; p++) { if (A[p] == exist) { int j = p*p; while (j <= n) { if (A[j] == exist) //只有沒被去除,才做去除操作。避免重復統計 { A[j] = not_exist; count--; //減少1個 } j += p; } } } delete[] A; return count; }
這個算法的速度比我的這篇文章中最好的solution還要快10倍,很是強悍。但有個缺點就是空間復雜度為O(n)。
使用這個算法解決leetcode題目后的runtime統計,可以發現這個算法超越了98.89%的Ac。