O(n) 的算法居然超時了,此時的 n 究竟是多大?







一些同學可能對計算機運行的速度還沒有概念,就是感覺計算機運行速度應該會很快,那麼在leetcode上做算法題目的時候爲什麼會超時呢?

計算機究竟1s可以執行多少次操作呢?接下來探討一下這個問題。

超時是怎麼回事

大家在leetcode上練習算法的時候應該都遇到過一種錯誤是“超時”。

也就是說程序運行的時間超過了規定的時間,一般OJ(online judge)的超時時間就是1s,也就是用例數據輸入後最多要1s內得到結果,暫時還不清楚leetcode的判題規則,下文爲了方便講解,暫定超時時間就是1s。

如果寫出了一個O(n)的算法 ,其實可以估算出來n是多大的時候算法的執行時間就會超過1s了。

如果n的規模已經足夠讓O(n)的算法運行時間超過了1s,就應該考慮log(n)的解法了。

從硬件配置看計算機的性能

計算機的運算速度主要看CPU的配置,以2015年MacPro爲例,CPU配置:2.7 GHz Dual-Core Intel Core i5 。

也就是 2.7 GHz 奔騰雙核,i5處理器,GHz是指什麼呢,1Hz = 1/s,1Hz 是CPU的一次脈衝(可以理解爲一次改變狀態,也叫時鐘週期),稱之爲爲赫茲,那麼1GHz等於多少赫茲呢

  • 1GHz(兆赫)= 1000MHz(兆赫)
  • 1MHz(兆赫)= 1百萬赫茲

所以 1GHz = 10億Hz,表示CPU可以一秒脈衝10億次(有10億個時鐘週期),這裏不要簡單理解一個時鐘週期就是一次CPU運算。

例如1 + 2 = 3,cpu要執行四次才能完整這個操作,步驟一:把1放入寄存機,步驟二:把2放入寄存器,步驟三:做加法,步驟四:保存3。

而且計算機的cpu也不會只運行我們自己寫的程序上,同時cpu也要執行計算機的各種進程任務等等,我們的程序僅僅是其中的一個進程而已。

所以我們的程序在計算機上究竟1s真正能執行多少次操作呢?

做個測試實驗

在寫測試程序測1s內處理多大數量級數據的時候,有三點需要注意:

  • CPU執行每條指令所需的時間實際上並不相同,例如CPU執行加法和乘法操作的耗時實際上都是不一樣的。
  • 現在大多計算機系統的內存管理都有緩存技術,所以頻繁訪問相同地址的數據和訪問不相鄰元素所需的時間也是不同的。
  • 計算機同時運行多個程序,每個程序裏還有不同的進程線程在搶佔資源。

儘管有很多因素影響,但是還是可以對自己程序的運行時間有一個大體的評估的。

引用算法4裏面的一段話:

  • 火箭科學家需要大致知道一枚試射火箭的着陸點是在大海里還是在城市中;
  • 醫學研究者需要知道一次藥物測試是會殺死還是會治癒實驗對象;

所以「任何開發計算機程序員的軟件工程師都應該能夠估計這個程序的運行時間是一秒鐘還是一年」

這個是最基本的,所以以上誤差就不算事了。

以下以C++代碼爲例:

測試硬件:2015年MacPro,CPU配置:2.7 GHz Dual-Core Intel Core i5

實現三個函數,時間複雜度分別是 O(n) , O(n^2), O(nlogn),使用加法運算來統一測試。

// O(n)
void function1(long long n) {
    long long k = 0;
    for (long long i = 0; i < n; i++) {
        k++;
    }
}

// O(n^2)
void function2(long long n) {
    long long k = 0;
    for (long long i = 0; i < n; i++) {
        for (long j = 0; j < n; j++) {
            k++;
        }
    }

}
// O(nlogn)
void function3(long long n) {
    long long k = 0;
    for (long long i = 0; i < n; i++) {
        for (long long j = 1; j < n; j = j * 2) { // 注意這裏j=1
            k++;
        }
    }
}

來看一下這三個函數隨着n的規模變化,耗時會產生多大的變化,先測function1 ,就把 function2 和 function3 註釋掉

int main() {
    long long n; // 數據規模
    while (1) {
        cout << "輸入n:";
        cin >> n;
        milliseconds start_time = duration_cast<milliseconds >(
            system_clock::now().time_since_epoch()
        );
        function1(n);
//        function2(n);
//        function3(n);
        milliseconds end_time = duration_cast<milliseconds >(
            system_clock::now().time_since_epoch()
        );
        cout << "耗時:" << milliseconds(end_time).count() - milliseconds(start_time).count()
            <<" ms"<< endl;
    }
}

來看一下運行的效果,如下圖:

O(n)的算法,1s內大概計算機可以運行 5 * (10^8)次計算,可以推測一下O(n^2) 的算法應該1s可以處理的數量級的規模是 5 * (10^8)開根號,實驗數據如下。

O(n^2)的算法,1s內大概計算機可以運行 22500次計算,驗證了剛剛的推測。

在推測一下O(nlogn)的話, 1s可以處理的數據規模是什麼呢?

理論上應該是比 O(n)少一個數量級,因爲logn的複雜度 其實是很快,看一下實驗數據。


O(nlogn)的算法,1s內大概計算機可以運行 2 * (10^7)次計算,符合預期。

這是在我個人PC上測出來的數據,不能說是十分精確,但數量級是差不多的,大家也可以在自己的計算機上測一下。

「整體測試數據整理如下:」


至於O(logn) 和O(n^3) 等等這些時間複雜度在1s內可以處理的多大的數據規模,大家可以自己寫一寫代碼去測一下了。

完整測試代碼

#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
using namespace chrono;
// O(n)
void function1(long long n) {
    long long k = 0;
    for (long long i = 0; i < n; i++) {
        k++;
    }
}

// O(n^2)
void function2(long long n) {
    long long k = 0;
    for (long long i = 0; i < n; i++) {
        for (long j = 0; j < n; j++) {
            k++;
        }
    }

}
// O(nlogn)
void function3(long long n) {
    long long k = 0;
    for (long long i = 0; i < n; i++) {
        for (long long j = 1; j < n; j = j * 2) { // 注意這裏j=1
            k++;
        }
    }
}
int main() {
    long long n; // 數據規模
    while (1) {
        cout << "輸入n:";
        cin >> n;
        milliseconds start_time = duration_cast<milliseconds >(
            system_clock::now().time_since_epoch()
        );
        function1(n);
//        function2(n);
//        function3(n);
        milliseconds end_time = duration_cast<milliseconds >(
            system_clock::now().time_since_epoch()
        );
        cout << "耗時:" << milliseconds(end_time).count() - milliseconds(start_time).count()
            <<" ms"<< endl;
    }
}


總結

本文詳細分析了在leetcode上做題程序爲什麼會有超時,以及從硬件配置上大體知道CPU的執行速度,然後親自做一個實驗來看看O(n)的算法,跑一秒鐘,這個n究竟是做大,最後給出不同時間複雜度,一秒內可以運算出來的n的大小。

建議大家也都自己做一做實驗,測一測,看看是不是和我的測出來的結果差不多。

這樣,大家應該對程序超時時候的數據規模有一個整體的認識了。

本文分享自微信公衆號 - 五分鐘學算法(CXYxiaowu)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章