如果一個正整數只有兩個因子, 1和p,則稱p爲素數.
2. 改進, 去掉偶數的判斷
時間複雜度O(n/2), 速度提高一倍.
3. 進一步減少判斷的範圍
定理: 如果n不是素數, 則n有滿足1<d<=sqrt(n)的一個因子d.
證明: 如果n不是素數, 則由定義n有一個因子d滿足1<d<n.
如果d大於sqrt(n), 則n/d是滿足1<n/d<=sqrt(n)的一個因子.
例如: 11%3 != 0 可以確定 11%(3*i) != 0.
定理: 如果n不是素數, 則n有滿足1<d<=sqrt(n)的一個"素數"因子d.
證明: I1. 如果n不是素數, 則n有滿足1<d<=sqrt(n)的一個因子d.
I2. 如果d是素數, 則定理得證, 算法終止.
I3. 令n=d, 並轉到步驟I1.
由於不可能無限分解n的因子, 因此上述證明的算法最終會停止.
函數PI(x)滿足素數定理: ln(x)-3/2 < x/PI(x) < ln(x)-1/2, 當x >= 67時.
因此O(PI(sqrt(n)))可以表示爲O(sqrt(x)/(ln(sqrt(x))-3/2)),
O(sqrt(x)/(ln(sqrt(x))-3/2))也是這個算法的空間複雜度.
5. 構造素數序列primes[i]: 2, 3, 5, 7, ...
由4的算法我們知道, 在素數序列已經被構造的情況下, 判斷n是否爲素數效率很高;
但是, 在構造素數序列本身的時候, 是否也可是達到最好的效率呢?
事實上這是可以的! -- 我們在構造的時候完全可以利用已經被構造的素數序列!
假設我們已經我素數序列: p1, p2, .. pn
現在要判斷pn+1是否是素數, 則需要(1, sqrt(pn+1)]範圍內的所有素數序列,
而這個素數序列顯然已經作爲p1, p2, .. pn的一個子集被包含了!
在一定的應用範圍內, 我們可以把近似認爲makePrimes需要常數時間.
在後面的討論中, 我們將探討一種對計算機而言更好的makePrimes方法.
6. 更好地利用計算機資源...
當前的主流PC中, 一個整數的大小爲2^32. 如果需要判斷2^32大小的數是否爲素數,
則可能需要測試[2, 2^16]範圍內的所有素數(2^16 == sqrt(2^32)).
由4中提到的素數定理我們可以大概確定[2, 2^16]範圍內的素數個數.
由於2^16/(ln(2^16)-1/2) = 6138, 2^16/(ln(2^16)-3/2) = 6834,
我們可以大概估計出[2, 2^16]範圍內的素數個數6138 < PI(2^16) < 6834.
在對[2, 2^16]範圍內的素數進行統計, 發現只有6542個素數:
p_6542: 65521, 65521^2 = 4293001441 < 2^32, (2^32 = 4294967296)
p_6543: 65537, 65537^2 = 4295098369 > 2^32, (2^32 = 4294967296)
在實際運算時unsigned long x = 4295098369;將發生溢出, 爲131073.
在程序中, 我是採用double類型計算得到的結果.
分析到這裏我們可以看到, 我們只需要緩衝6543個素數, 我們就可以採用4中的算法
高效率地判斷[2, 2^32]如此龐大範圍內的素數!
(原本的2^32大小的問題規模現在已經被減小到6543規模了!)
雖然用現在的計算機處理[2, 2^16]範圍內的6542個素數已經沒有一點問題,
雖然makePrimes只要被運行一次就可以, 但是我們還是考慮一下是否被改進的可能?!
我想學過java的人肯定想把makePrimes作爲一個靜態的初始化實現, 在C++中也可以
模擬java中靜態的初始化的類似實現:
#define NELEMS(x) ((sizeof(x)) / (sizeof((x)[0])))
static int primes[6542+1];
static struct _Init { _Init(){makePrimes(primes, NELEMS(primes);} } _init;
如此, 就可以在程序啓動的時候自動掉用makePrimes初始化素數序列.
但, 我現在的想法是: 爲什麼我們不能在編譯的時候調用makePrimes函數呢?
完全可以!!! 代碼如下:
(我覺得叫O(0)可能更合適!)
7. 二分法查找
現在我們緩存了前大約sqrt(2^32)/(ln(sqrt(2^32)-3/2))個素數列表, 在判斷2^32級別的
素數時最多也只需要PI(sqrt(2^32))次判斷(準確值是6543次), 但是否還有其他的方式判斷呢?
當素數比較小的時候(不大於2^16), 是否可以直接從緩存的素數列表中直接查詢得到呢?
答案是肯定的! 由於primes是一個有序的數列, 因此我們當素數小於2^16時, 我們可以直接
採用二分法從primes中查詢得到(如果查詢失敗則不是素數).
if(n <= primes[NELEMS(primes)-1] && n >= 67): O(log2(NELEMS(primes))) < 13;
if(n > primes[NELEMS(primes)-1]): O(PI(sqrt(n))) <= NELEMS(primes).
8. 素數定理+2分法查找
在7中, 我們對小等於primes[NELEMS(primes)-1]的數採用2分法查找進行判斷.
我們之前針對2^32緩衝的6453個素數需要判斷的次數爲13次(log2(1024*8) == 13).
對於小的素數而言(其實就是2^16範圍只內的數), 13次的比較已經完全可以接受了.
不過根據素數定理: ln(x)-3/2 < x/PI(x) < ln(x)-1/2, 當x >= 67時, 我們依然
可以進一步縮小小於2^32情況的查找範圍(現在是0到NELEMS(primes)-1範圍查找).
我們需要解決問題是(n <= primes[NELEMS(primes)-1):
如果n爲素數, 那麼它在素數序列可能出現的範圍在哪?
---- (n/(ln(n)-1/2), n/(ln(n)-3/2)), 即素數定理!
上面的代碼修改如下:
if(n <= primes[NELEMS(primes)-1] && n >= 67): O(log2(hi-lo))) < ???;
if(n > primes[NELEMS(primes)-1]): O(PI(sqrt(n))) <= NELEMS(primes).
9. 打包成素數庫(給出全部的代碼)
到目前爲止, 我已經給出了我所知道所有改進的方法(如果有人有更好的算法感謝告訴我).
這裏需要強調的一點是, 這裏討論的素數求法是針對0-2^32範圍的數而言, 至於像尋找
成百上千位大小的數不在此討論範圍, 那應該算是純數學的內容了.
代碼保存在2個文件: prime.h, prime.cpp.
到這裏, 關於素數的討論基本告一段落. 回顧我們之前的求解過程, 我們會發現
如果缺少數學的基本知識會很難設計好的算法; 但是如果一味地只考慮數學原理,
而忽律了計算機的本質特徵, 也會有同樣的問題.