讀完本文,你能夠去力扣拿下以下題目:java
204.計數質數git
-----------github
素數的定義看起來很簡單,若是一個數若是隻能被 1 和它自己整除,那麼這個數就是素數。算法
不要以爲素數的定義簡單,恐怕沒多少人真的能把素數相關的算法寫得高效。好比讓你寫這樣一個函數:數組
// 返回區間 [2, n) 中有幾個素數 int countPrimes(int n) // 好比 countPrimes(10) 返回 4 // 由於 2,3,5,7 是素數
你會如何寫這個函數?我想你們應該會這樣寫:函數
int countPrimes(int n) { int count = 0; for (int i = 2; i < n; i++) if (isPrim(i)) count++; return count; } // 判斷整數 n 是不是素數 boolean isPrime(int n) { for (int i = 2; i < n; i++) if (n % i == 0) // 有其餘整除因子 return false; return true; }
這樣寫的話時間複雜度 O(n^2),問題很大。首先你用 isPrime 函數來輔助的思路就不夠高效;並且就算你要用 isPrime 函數,這樣寫算法也是存在計算冗餘的。優化
先來簡單說下若是你要判斷一個數是否是素數,應該如何寫算法。只需稍微修改一下上面的 isPrim 代碼中的 for 循環條件:code
boolean isPrime(int n) { for (int i = 2; i * i <= n; i++) ... }
換句話說,i
不須要遍歷到 n
,而只須要到 sqrt(n)
便可。爲何呢,咱們舉個例子,假設 n = 12
。leetcode
12 = 2 × 6 12 = 3 × 4 12 = sqrt(12) × sqrt(12) 12 = 4 × 3 12 = 6 × 2
能夠看到,後兩個乘積就是前面兩個反過來,反轉臨界點就在 sqrt(n)
。get
換句話說,若是在 [2,sqrt(n)]
這個區間以內沒有發現可整除因子,就能夠直接判定 n
是素數了,由於在區間 [sqrt(n),n]
也必定不會發現可整除因子。
如今,isPrime
函數的時間複雜度降爲 O(sqrt(N)),可是咱們實現 countPrimes
函數其實並不須要這個函數,以上只是但願讀者明白 sqrt(n)
的含義,由於等會還會用到。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
countPrimes
高效解決這個問題的核心思路是和上面的常規思路反着來:
首先從 2 開始,咱們知道 2 是一個素數,那麼 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8... 都不多是素數了。
而後咱們發現 3 也是素數,那麼 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12... 也都不多是素數了。
看到這裏,你是否有點明白這個排除法的邏輯了呢?先看咱們的初版代碼:
int countPrimes(int n) { boolean[] isPrim = new boolean[n]; // 將數組都初始化爲 true Arrays.fill(isPrim, true); for (int i = 2; i < n; i++) if (isPrim[i]) // i 的倍數不多是素數了 for (int j = 2 * i; j < n; j += i) isPrim[j] = false; int count = 0; for (int i = 2; i < n; i++) if (isPrim[i]) count++; return count; }
若是上面這段代碼你可以理解,那麼你已經掌握了總體思路,可是還有兩個細微的地方能夠優化。
首先,回想剛纔判斷一個數是不是素數的 isPrime
函數,因爲因子的對稱性,其中的 for 循環只須要遍歷 [2,sqrt(n)]
就夠了。這裏也是相似的,咱們外層的 for 循環也只須要遍歷到 sqrt(n)
:
for (int i = 2; i * i < n; i++) if (isPrim[i]) ...
除此以外,很難注意到內層的 for 循環也能夠優化。咱們以前的作法是:
for (int j = 2 * i; j < n; j += i) isPrim[j] = false;
這樣能夠把 i
的整數倍都標記爲 false
,可是仍然存在計算冗餘。
好比 n = 25
,i = 4
時算法會標記 4 × 2 = 8,4 × 3 = 12 等等數字,可是這兩個數字已經被 i = 2
和 i = 3
的 2 × 4 和 3 × 4 標記了。
咱們能夠稍微優化一下,讓 j
從 i
的平方開始遍歷,而不是從 2 * i
開始:
for (int j = i * i; j < n; j += i) isPrim[j] = false;
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
這樣,素數計數的算法就高效實現了,其實這個算法有一個名字,叫作 Sieve of Eratosthenes。看下完整的最終代碼:
int countPrimes(int n) { boolean[] isPrim = new boolean[n]; Arrays.fill(isPrim, true); for (int i = 2; i * i < n; i++) if (isPrim[i]) for (int j = i * i; j < n; j += i) isPrim[j] = false; int count = 0; for (int i = 2; i < n; i++) if (isPrim[i]) count++; return count; }
該算法的時間複雜度比較難算,顯然時間跟這兩個嵌套的 for 循環有關,其操做數應該是:
n/2 + n/3 + n/5 + n/7 + ...
= n × (1/2 + 1/3 + 1/5 + 1/7...)
括號中是素數的倒數。其最終結果是 O(N * loglogN),有興趣的讀者能夠查一下該算法的時間複雜度證實。
以上就是素數算法相關的所有內容。怎麼樣,是否是看似簡單的問題卻有很多細節能夠打磨呀?
_____________
個人 在線電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 算法倉庫 已經得到了 70k star,歡迎標星!