解決一道leetcode算法題的曲折過程及引發的思考


寫在前面

本題實際解題過程是 從 40秒 --> 24秒 -->1.5秒 --> 715ms --> 320ms --> 48ms --> 36ms --> 28ms

最后以28ms的運行速度告結, 有更好的解法或者減少算法復雜度的博友可以給我發消息也可以評論, 我們一起討論.


 

 

第一次在leetcode解算法題,想來個開門紅,先挑選一道簡單的題目AC了再說。leetcode對每道題都有一個難度提示,分別是easy,medium,hard

於是在第一頁中看到了幾道標着easy的題目,突然看到一道easy題目的通過率只有 18.6%,也是挺低的,在好奇心的驅使下點了進去。

題目的大概意思是:

  給定一個數值n,算出從0到n之間有多少個數是質數

這種題目的通過率只有18.6%?看到題目后我就覺得太簡單了,明顯是水題啊,於是着手做了起來,結果歷經曲折,以下是我的四次嘗試過程……

examination questions


 

Count Primes

Description:

Count the number of prime numbers less than a non-negative number, n

 

Please use the following function to solve the problem:

int countPrimes(int n){

}


 

第一次嘗試解決

我的思路:先了解質數的判斷定義:除了1和它本身外,不能被其他自然數整除。從這個判斷定義上來看,用編程實現很簡單。於是……

轉換成編程思想:

The first step:給定一個n,對於大於2的數,從2開始,通過for循環,依次判斷是否為質數,直到n-1

the second step:對每個要判斷是否為質數的數,再進行從2到(自身-1)直接的數進行for循環遍歷檢查,一旦發現可以被某個數整除,則跳出,該數不是質數,進行下一個數的檢查。

代碼實現:

#include <stdio.h>

int countPrimes(int n) {
    if (n == 0 || n == 1 || n == 2){
        printf("0\n");
        return 0;
    }
    int temp = 0;
    int flag = 0;
    for (int i = 2; i < n; i++){
        for (int j = 2; j < i; j++){
            if (i%j == 0){
                flag = 1;
                break;
            }
        }
        if (flag == 0){
            temp++;
        }
        else{
            flag = 0;
        }
    }

    printf("%d\n",temp);
    return 0;
}

int main(){
    int n;
    scanf("%d",&n);
    countPrimes(n);

    return 0;
}

自己找了幾個值測試了一下

測試如下:

輸入54 正確結果是16

結果正確

輸入174 正確結果是 40

結果正確

 

都正確,這么簡單的題?

於是趕緊去提交答案

結果顯示:

 

失敗!

用的時間太長了,測試值是49969

於是我用49969測試了一下,結果用了40秒才運算完成,這也太久了,肯定通不過,想到之前看到的通過率,我想我大概知道這道題其實沒那么簡單,leetcode也不可能水。

 

 

第二次嘗試解決

我知道那個for循環,如果數值非常大的話,那個運算量會很大,而且測試了很多不可能的值,比如 n = 40,他不可能被 大於 40/2 ~ 40之間的數整除吧,也就是從21到40之間的數值可以直接去掉,不用計算了,然后我就去掉這一部分,代碼修改如下:

for (int j = 2; j < i; j++){
            if (i%j == 0){
                flag = 1;
                break;
            }
}

改成:

for (int j = 2; j < i/2; j++){ 
            if (i%j == 0){
                flag = 1;
                break;
            }
}

 

完整代碼:

#include <stdio.h>

int countPrimes(int n) {
    if (n == 0 || n == 1 || n == 2){
        printf("0\n");
        return 0;
    }
    int temp = 0;
    int flag = 0;
    for (int i = 2; i < n; i++){
        for (int j = 2; j < i/2; j++){
            if (i%j == 0){
                flag = 1;
                break;
            }
        }
        if (flag == 0){
            temp++;
        }
        else{
            flag = 0;
        }
    }

    printf("%d\n",temp);
    return 0;
}

int main(){
    int n;
    scanf("%d",&n);
    countPrimes(n);

    return 0;
}

提交答案:

 

失敗!

自己用49969這個值測試了一下,結果用時24秒

好吧,雖然是比之前40秒少了不少,但是時間還是太長了啊

 

第三次嘗試解決

通過第二次嘗試解決聯想一下,上面的情況是把n除以2之后,把后面一大部分去去掉(比如40/2 = 20 ,然后去掉20~40之間的數,不檢查),那么除以3,除以4呢,是否可以以此類推?

通過運算驗證,這個算法思路是正確的

那么從2開始,如果不能被整除,然后去掉后面1-1/2部分(對於40來說,就是20~40部分,大約20個數)

再從3開始,然后去掉后面1-1/3部分(對40來說,就是13~40部分,結合上面一步,本次省略了 13~20,也就是大約7個數)

在從4開始……

如此一來,這樣就可以減少了大量的不必要的測試項了!

做法:引入變量t = 1,每檢查一次,t = t+1,然后 i = j / i

int t;
    for (int i = 2; i < n; i++){
        t = 1;
        for (int j = 2; j < i/t; j++){
            if (i%j == 0){
                flag = 1;
                break;
            }
            t++;
        }
        if (flag == 0){
            temp++;
        }
        else{
            flag = 0;
        }
    }

完整代碼:

#include <stdio.h>

int countPrimes(int n) {
    if (n == 0 || n == 1 || n == 2){
        printf("0\n");
        return 0;
    }
    int temp = 0;
    int flag = 0;
    int t;
    for (int i = 2; i < n; i++){
        t = 1;
        for (int j = 2; j < i/t; j++){
            if (i%j == 0){
                flag = 1;
                break;
            }
            t++;
        }
        if (flag == 0){
            temp++;
        }
        else{
            flag = 0;
        }
    }

    printf("%d\n",temp);
    return 0;
}

int main(){
    int n;
    scanf("%d",&n);
    countPrimes(n);

    return 0;
}

測試了 49969這個值,結果很快就出來,不到1秒鍾!

這下可以通過了吧,應該可以AC了

提交一下:

失敗!

怎么回事!這也不行!這leetcode的要求這么高?難道它的時間要求設置為0.1秒這樣的級別?!

這次提示是 1500000 這個值測試不得通過,時間還是太長了。居然拿這么大的值來測試!

本機測試了一下,用時大約2秒(光標閃了兩下的時間)就出來了,不過對於嚴格的leetcode來說,時間還是太長了。

 

第四次嘗試解決

我意識到這個問題不能用上面的算法來解決,因為這個算法對於那個 質數的判斷定義 來說,感覺已經是最優算法了。只能重新設計算法,於是把代碼全部刪了,重新再來!

拿起草稿紙,好好思考一下如何解決,思考存在什么更好的方法以及規律,思考發現了一些規律如下:

比如18,它能被2,3,6,9 整除,而6又能被2整除,所以在檢測到被6整除之前,肯定已經檢查到可以被2整除了,已經break了,所以我發現,凡是2的倍數,只要用2來判斷就好了,也就是說2代表了所有的雙數。同樣的,3也是,有了3,就不用9來檢查了,能被9整除的肯定能被3整除。

於是我們來觀察下面一組數字:

2,3,4,5,6,7,8,9,10,11

2符合要求

3符合要求

4由於是2的倍數,所以不符合要求

5符合要求

6由於是2的倍數,所以不符合要求

7符合要求

8由於是2的倍數,所以不符合要求

9由於是3的倍數,所以不符合要求

10由於是2的倍數,所以不符合要求

11符合要求

我們可以發現,符合要求的都是質數(關於用數學來證明這個結論,不是討論的重點)

也就是說,我們僅僅需要檢測一個數是否能被比它小的質數整除,如果可以,說明它不是質數,如果都不可以,說明他是質數

轉換成編程思想:

  用一個數組arr來存放這些質數,先給定arr[0] 為2,初始化,再用2來判斷下一個值是否是質數,如果是,那么arr[1] == 該質數,再檢查下一個數,檢查是否可以被arr[0] 和 arr[1] 整除,以此類推

 

分析:下面代碼定義的數組長度為200,不用太大,因為第200個質數的值是1223,那么它的平方是:1495729,對於n = 1500000來說,綽綽有余了

當然,為了保守起見,這個值可以設置大一點。

為了提高運算速度,這里限制了作為基數的質數個數為200個,超過200個部分,不進行運算,因為也沒必要。

if (flag == false){
            if (k < 199){
                k++;
                arr[k] = i;
            }
            temp++;
}

代碼實現:

#include <stdio.h>

int countPrimes(int n) {
    if (n == 0 || n == 1 || n == 2){
        return 0;
    }
    if (n == 3){
        return 1;
    }

    int temp = 0;
    bool flag = false;
    int arr[200] = { '\0' };
    int k = 0;
    arr[0] = 2;

    for (int i = 3; i < n; i++){
        for (int j = 0; j <= k; j++){
            if (i%arr[j] == 0){
                flag = true;
                break;
            }
        }

        if (flag == false){
            if (k < 199){
                k++;
                arr[k] = i;
            }
            temp++;
        }
        else{
            flag = false;
        }
    }

    return temp+1;
}

int main(){
    int n;
    scanf("%d",&n);
    printf("%d\n",countPrimes(n));

    return 0;
}

 

用時大約0.1秒,這下應該可以通過了。

提交一下:

成功!

終於通過了!還記得當初認為它是水題嗎?看來通過率低原來是這么來的……

 

 

引發的思考

從運行時間上來看本題的運算提升過程: 從 40秒 --> 24秒 -->1.5秒 --> 715ms --> 320ms --> 48ms --> 36ms --> 28ms

01.秒 與 40秒的差距是400倍,一個好的算法和解決方案,可以節省400倍的時間!

在我們平時做項目或者為了完成某些功能達到某個結果的時候,也許僅僅是為了達到而達到,結果正確就ok,但是很多時候,這種情況下可能僅僅滿足自己的要求或者是小范圍內符合要求,在很廣的面上來看,很有可能完全不能用。

比如數據庫設計,設計得好的,很快就可以查詢到所要的數據。設計不好的,在短期內可能發現不了什么問題,但是到了數據龐大的時候,也許就沒有實用性。有前輩跟我講過,他有個項目是交水電費系統,測試的區域是一個小鎮,查詢速度不錯。后來推廣了,范圍擴大到一個市,結果查詢某一家的水電費情況,居然用了5分鍾才查出來,明顯是沒有做到合理的設計。

那么,如何從一開始就做到盡量防止存在后顧之憂呢?我思考如下:

第一,要對問題的本質有非常明確的了解,不能只知其一不知其二。比如上面我的解題過程,僅僅知道質數的判斷定義,並沒有對里面存在的規律進行深入了解,造成了解題失敗。

第二,在解決問題之前,盡可能的思考多種解法方案,記錄下來,分析各種方案在時間效率,空間使用度的優劣。再選上符合要求的最好的方案去實施。切忌想到一個方法,它可以解決問題,但是很有可能不是最優方法,但是為了快速解決問題而拒絕思考,最后可能會照成不必要麻煩。(重寫代碼,代價更高),這個道理可能大家都懂,但是很少人真正有這樣去做一件事情。

第三,學會站在巨人的肩膀上。本次解題,由於想要鍛煉自己的思維和練習手感,所以要求自己一定要獨立完成。但是在我們平時解決問題中,要學會借力。很多前人研究過的方法,他也許用了好長的時間才研究出來的從目前看最優的方法,那么我們學習他的方法,理解他的思維,用他的方法這樣我們可以快速解決問題,同時也把知識化為自己的,這樣效率最高,所以不要固執,學會使用google,讓自己站在巨人的肩膀上前行,收獲會是最大的。

 

 


 

2015-05-19 優化算法

=====================

有博友提出了用篩法解此題是最優的,於是去百度查了一下篩法,大概算法如下:

給出要篩數值的范圍n,找出n以內的素數p1,p2,p3,......,pk。先用2去篩,即把2留下,把2的倍數剔除掉;再用下一個素數,也就是3篩,把3留下,把3的倍數剔除掉;接下去用下一個素數5篩,把5留下,把5的倍數剔除掉;不斷重復下去......

 下面是我動手實現這個算法:

#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>

int countPrimes(int n) {
    if (n == 0 || n == 1 || n == 2){
        return 0;
    }

    bool *judge = (bool*)malloc(sizeof(bool)*n);
    memset(judge, true, sizeof(bool)*n);

    for (int i = 2; i < sqrt((double)n); i++){
        if (judge[i] == true){
            for (int j = 2; i*j < n; j++){
                judge[i*j] = false;
            }
        }
    }
    int temp = 0;
    for (int i = 2; i < n; i++){
        if (judge[i] == true){
            temp++;
        }
    }

    return temp;
}

int main(){
    int n;
    scanf("%d", &n);
    printf("%d\n",countPrimes(n));
    return 0;
}

LeetCode判斷結果:

<0.1秒 效率確實高了不少

 

說明

本題還可以再優化, 使用篩法, 本程序存在 重復賦值的情況, 如2的倍數有6, 3也有6, 他們會重復把下標為6的數組賦值為flase

解法有時間再思考


 

2015-05-19 再優化算法

=========================

int countPrimes(int n) {

    if (n == 0 || n == 1 || n == 2){
        return 0;
    }

    bool *judge = (bool*)malloc(sizeof(bool)*n);
    memset(judge, true, sizeof(bool)*n);

    int temp = 2;
    for (int i = 2; i < sqrt((double)n); i++){
        if (judge[i] == true){
            int t;

            for (int j = i; j * i < n; j++){
                t = j * i;
                if (judge[t] != false){
                    temp++;
                    judge[t] = false;
                }        
            }
        }
    }

    return (n-temp);
}

LeetCode 判斷結果:


 

 

2015-05-19 最后一次優化

==========================

解題代碼:

int countPrimes(int n) {

    if (n == 0 || n == 1 || n == 2){
        return 0;
    }

    bool *judge = (bool*)malloc(sizeof(bool)*n);
    memset(judge, true, sizeof(bool)*n);
    int t;

    int temp = 2;
    for (int i = 2; i < sqrt((double)n); i++){
        int j = i;
        t = i*j;

        if (judge[i]){    
            for (; t < n; j++){
                if (judge[t]){
                    temp++;
                    judge[t] = false;
                }        
                t = j * i;
            }
        }
    }

    return (n-temp);
}

LeetCode判斷結果:

 

最后一次, 28ms, 我想可以收工了, 我知道里面還有可以再次優化的, 在運算時間上可以再快的, 可以和我討論, 有好的算法歡迎評論留言

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM