如何高效尋找素?cái)?shù)

讀完本文嚎货,你可以去力扣拿下如下題目:

204.計(jì)數(shù)質(zhì)數(shù)

-----------

素?cái)?shù)的定義看起來很簡單,如果一個(gè)數(shù)如果只能被 1 和它本身整除梅鹦,那么這個(gè)數(shù)就是素?cái)?shù)突颊。

不要覺得素?cái)?shù)的定義簡單鲁豪,恐怕沒多少人真的能把素?cái)?shù)相關(guān)的算法寫得高效。比如讓你寫這樣一個(gè)函數(shù):

// 返回區(qū)間 [2, n) 中有幾個(gè)素?cái)?shù) 
int countPrimes(int n)

// 比如 countPrimes(10) 返回 4
// 因?yàn)?2,3,5,7 是素?cái)?shù)

你會(huì)如何寫這個(gè)函數(shù)律秃?我想大家應(yīng)該會(huì)這樣寫:

int countPrimes(int n) {
    int count = 0;
    for (int i = 2; i < n; i++)
        if (isPrim(i)) count++;
    return count;
}

// 判斷整數(shù) n 是否是素?cái)?shù)
boolean isPrime(int n) {
    for (int i = 2; i < n; i++)
        if (n % i == 0)
            // 有其他整除因子
            return false;
    return true;
}

這樣寫的話時(shí)間復(fù)雜度 O(n^2)爬橡,問題很大。首先你用 isPrime 函數(shù)來輔助的思路就不夠高效友绝;而且就算你要用 isPrime 函數(shù),這樣寫算法也是存在計(jì)算冗余的肝劲。

先來簡單說下如果你要判斷一個(gè)數(shù)是不是素?cái)?shù)迁客,應(yīng)該如何寫算法。只需稍微修改一下上面的 isPrim 代碼中的 for 循環(huán)條件:

boolean isPrime(int n) {
    for (int i = 2; i * i <= n; i++)
        ...
}

換句話說辞槐,i 不需要遍歷到 n掷漱,而只需要到 sqrt(n) 即可。為什么呢榄檬,我們舉個(gè)例子卜范,假設(shè) n = 12

12 = 2 × 6
12 = 3 × 4
12 = sqrt(12) × sqrt(12)
12 = 4 × 3
12 = 6 × 2

可以看到鹿榜,后兩個(gè)乘積就是前面兩個(gè)反過來海雪,反轉(zhuǎn)臨界點(diǎn)就在 sqrt(n)

換句話說舱殿,如果在 [2,sqrt(n)] 這個(gè)區(qū)間之內(nèi)沒有發(fā)現(xiàn)可整除因子奥裸,就可以直接斷定 n 是素?cái)?shù)了,因?yàn)樵趨^(qū)間 [sqrt(n),n] 也一定不會(huì)發(fā)現(xiàn)可整除因子沪袭。

現(xiàn)在湾宙,isPrime 函數(shù)的時(shí)間復(fù)雜度降為 O(sqrt(N)),但是我們實(shí)現(xiàn) countPrimes 函數(shù)其實(shí)并不需要這個(gè)函數(shù)冈绊,以上只是希望讀者明白 sqrt(n) 的含義侠鳄,因?yàn)榈葧?huì)還會(huì)用到。

PS:我認(rèn)真寫了 100 多篇原創(chuàng)死宣,手把手刷 200 道力扣題目伟恶,全部發(fā)布在 labuladong的算法小抄,持續(xù)更新毅该。建議收藏知押,按照我的文章順序刷題叹螟,掌握各種算法套路后投再入題海就如魚得水了。

高效實(shí)現(xiàn) countPrimes

高效解決這個(gè)問題的核心思路是和上面的常規(guī)思路反著來:

首先從 2 開始台盯,我們知道 2 是一個(gè)素?cái)?shù)罢绽,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8... 都不可能是素?cái)?shù)了。

然后我們發(fā)現(xiàn) 3 也是素?cái)?shù)静盅,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12... 也都不可能是素?cái)?shù)了良价。

看到這里,你是否有點(diǎn)明白這個(gè)排除法的邏輯了呢蒿叠?先看我們的第一版代碼:

int countPrimes(int n) {
    boolean[] isPrim = new boolean[n];
    // 將數(shù)組都初始化為 true
    Arrays.fill(isPrim, true);

    for (int i = 2; i < n; i++) 
        if (isPrim[i]) 
            // i 的倍數(shù)不可能是素?cái)?shù)了
            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;
}

如果上面這段代碼你能夠理解明垢,那么你已經(jīng)掌握了整體思路,但是還有兩個(gè)細(xì)微的地方可以優(yōu)化市咽。

首先痊银,回想剛才判斷一個(gè)數(shù)是否是素?cái)?shù)的 isPrime 函數(shù),由于因子的對稱性施绎,其中的 for 循環(huán)只需要遍歷 [2,sqrt(n)] 就夠了溯革。這里也是類似的,我們外層的 for 循環(huán)也只需要遍歷到 sqrt(n)

for (int i = 2; i * i < n; i++) 
    if (isPrim[i]) 
        ...

除此之外谷醉,很難注意到內(nèi)層的 for 循環(huán)也可以優(yōu)化致稀。我們之前的做法是:

for (int j = 2 * i; j < n; j += i) 
    isPrim[j] = false;

這樣可以把 i 的整數(shù)倍都標(biāo)記為 false,但是仍然存在計(jì)算冗余俱尼。

比如 n = 25抖单,i = 4 時(shí)算法會(huì)標(biāo)記 4 × 2 = 8,4 × 3 = 12 等等數(shù)字遇八,但是這兩個(gè)數(shù)字已經(jīng)被 i = 2i = 3 的 2 × 4 和 3 × 4 標(biāo)記了矛绘。

我們可以稍微優(yōu)化一下,讓 ji 的平方開始遍歷刃永,而不是從 2 * i 開始:

for (int j = i * i; j < n; j += i) 
    isPrim[j] = false;

PS:我認(rèn)真寫了 100 多篇原創(chuàng)蔑歌,手把手刷 200 道力扣題目,全部發(fā)布在 labuladong的算法小抄揽碘,持續(xù)更新次屠。建議收藏,按照我的文章順序刷題雳刺,掌握各種算法套路后投再入題海就如魚得水了劫灶。

這樣,素?cái)?shù)計(jì)數(shù)的算法就高效實(shí)現(xiàn)了掖桦,其實(shí)這個(gè)算法有一個(gè)名字本昏,叫做 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;
}

該算法的時(shí)間復(fù)雜度比較難算涌穆,顯然時(shí)間跟這兩個(gè)嵌套的 for 循環(huán)有關(guān)怔昨,其操作數(shù)應(yīng)該是:

n/2 + n/3 + n/5 + n/7 + ...
= n × (1/2 + 1/3 + 1/5 + 1/7...)

括號中是素?cái)?shù)的倒數(shù)。其最終結(jié)果是 O(N * loglogN)宿稀,有興趣的讀者可以查一下該算法的時(shí)間復(fù)雜度證明趁舀。

以上就是素?cái)?shù)算法相關(guān)的全部內(nèi)容。怎么樣祝沸,是不是看似簡單的問題卻有不少細(xì)節(jié)可以打磨呀矮烹?

_____________

我的 在線電子書 有 100 篇原創(chuàng)文章,手把手帶刷 200 道力扣題目罩锐,建議收藏奉狈!對應(yīng)的 GitHub 算法倉庫 已經(jīng)獲得了 70k star,歡迎標(biāo)星涩惑!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仁期,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子竭恬,更是在濱河造成了極大的恐慌跛蛋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萍聊,死亡現(xiàn)場離奇詭異问芬,居然都是意外死亡悦析,警方通過查閱死者的電腦和手機(jī)寿桨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來强戴,“玉大人亭螟,你說我怎么就攤上這事∑锎酰” “怎么了预烙?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長道媚。 經(jīng)常有香客問我扁掸,道長,這世上最難降的妖魔是什么最域? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任谴分,我火速辦了婚禮,結(jié)果婚禮上镀脂,老公的妹妹穿的比我還像新娘牺蹄。我一直安慰自己,他們只是感情好薄翅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布沙兰。 她就那樣靜靜地躺著氓奈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鼎天。 梳的紋絲不亂的頭發(fā)上舀奶,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機(jī)與錄音训措,去河邊找鬼伪节。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绩鸣,可吹牛的內(nèi)容都是我干的怀大。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼呀闻,長吁一口氣:“原來是場噩夢啊……” “哼化借!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捡多,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蓖康,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后垒手,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒜焊,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年科贬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泳梆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡榜掌,死狀恐怖优妙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情憎账,我是刑警寧澤套硼,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站胞皱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏反砌。R本人自食惡果不足惜呆贿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一冒晰、第九天 我趴在偏房一處隱蔽的房頂上張望壶运。 院中可真熱鬧,春花似錦棵癣、人聲如沸狈谊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽务甥。三九已至催享,卻和暖如春因妙,著一層夾襖步出監(jiān)牢的瞬間攀涵,已是汗流浹背蜗细。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工踪区, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缎岗,地道東北人传泊。 一個(gè)月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓鹃祖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親池磁。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容