(數據結構)十分鐘搞定時間復雜度(算法的時間復雜度)

我們假設計算機運行一行基礎代碼需要執行一次運算。

int aFunc(void) {
    printf("Hello, World!\n");      //  需要執行 1 次
    return 0;       // 需要執行 1 次
}

那么上面這個方法需要執行 2 次運算

int aFunc(int n) {
    for(int i = 0; i<n; i++) {         // 需要執行 (n + 1) 次
        printf("Hello, World!\n");      // 需要執行 n 次
    }
    return 0;       // 需要執行 1 次
}

這個方法需要 (n + 1 + n + 1) = 2n + 2 次運算。

我們把 算法需要執行的運算次數 用 輸入大小n 的函數 表示,即 T(n) 。
此時為了 估算算法需要的運行時間 和 簡化算法分析,我們引入時間復雜度的概念。

定義:存在常數 c 和函數 f(N),使得當 N >= c 時 T(N) <= f(N),表示為 T(n) = O(f(n)) 。
如圖:


當 N >= 2 的時候,f(n) = n^2 總是大于 T(n) = n + 2 的,于是我們說 f(n) 的增長速度是大于或者等于 T(n) 的,也說 f(n) 是 T(n) 的上界,可以表示為 T(n) = O(f(n))。

因為f(n) 的增長速度是大于或者等于 T(n) 的,即T(n) = O(f(n)),所以我們可以用 f(n) 的增長速度來度量 T(n) 的增長速度,所以我們說這個算法的時間復雜度是 O(f(n))。

算法的時間復雜度,用來度量算法的運行時間,記作: T(n) = O(f(n))。它表示隨著 輸入大小n 的增大,算法執行需要的時間的增長速度可以用 f(n) 來描述。

顯然如果 T(n) = n^2,那么 T(n) = O(n^2),T(n) = O(n^3),T(n) = O(n^4) 都是成立的,但是因為第一個 f(n) 的增長速度與 T(n) 是最接近的,所以第一個是最好的選擇,所以我們說這個算法的復雜度是 O(n^2) 。

那么當我們拿到算法的執行次數函數 T(n) 之后怎么得到算法的時間復雜度呢?

  1. 我們知道常數項對函數的增長速度影響并不大,所以當 T(n) = c,c 為一個常數的時候,我們說這個算法的時間復雜度為 O(1);如果 T(n) 不等于一個常數項時,直接將常數項省略。
比如
第一個 Hello, World 的例子中 T(n) = 2,所以我們說那個函數(算法)的時間復雜度為 O(1)。
T(n) = n + 29,此時時間復雜度為 O(n)。
  1. 我們知道高次項對于函數的增長速度的影響是最大的。n^3 的增長速度是遠超 n^2 的,同時 n^2 的增長速度是遠超 n 的。 同時因為要求的精度不高,所以我們直接忽略低此項。
比如
T(n) = n^3 + n^2 + 29,此時時間復雜度為 O(n^3)。
  1. 因為函數的階數對函數的增長速度的影響是最顯著的,所以我們忽略與最高階相乘的常數。
比如
T(n) = 3n^3,此時時間復雜度為 O(n^3)。

綜合起來:如果一個算法的執行次數是 T(n),那么只保留最高次項,同時忽略最高項的系數后得到函數 f(n),此時算法的時間復雜度就是 O(f(n))。為了方便描述,下文稱此為 大O推導法。

由此可見,由執行次數 T(n) 得到時間復雜度并不困難,很多時候困難的是從算法通過分析和數學運算得到 T(n)。對此,提供下列四個便利的法則,這些法則都是可以簡單推導出來的,總結出來以便提高效率。

  1. 對于一個循環,假設循環體的時間復雜度為 O(n),循環次數為 m,則這個
    循環的時間復雜度為 O(n×m)。
void aFunc(int n) {
    for(int i = 0; i < n; i++) {         // 循環次數為 n
        printf("Hello, World!\n");      // 循環體時間復雜度為 O(1)
    }
}

此時時間復雜度為 O(n × 1),即 O(n)。

  1. 對于多個循環,假設循環體的時間復雜度為 O(n),各個循環的循環次數分別是a, b, c...,則這個循環的時間復雜度為 O(n×a×b×c...)。分析的時候應該由里向外分析這些循環。
void aFunc(int n) {
    for(int i = 0; i < n; i++) {         // 循環次數為 n
        for(int j = 0; j < n; j++) {       // 循環次數為 n
            printf("Hello, World!\n");      // 循環體時間復雜度為 O(1)
        }
    }
}

此時時間復雜度為 O(n × n × 1),即 O(n^2)。

  1. 對于順序執行的語句或者算法,總的時間復雜度等于其中最大的時間復雜度。
void aFunc(int n) {
    // 第一部分時間復雜度為 O(n^2)
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < n; j++) {
            printf("Hello, World!\n");
        }
    }
    // 第二部分時間復雜度為 O(n)
    for(int j = 0; j < n; j++) {
        printf("Hello, World!\n");
    }
}

此時時間復雜度為 max(O(n^2), O(n)),即 O(n^2)。

  1. 對于條件判斷語句,總的時間復雜度等于其中 時間復雜度最大的路徑 的時間復雜度。
void aFunc(int n) {
    if (n >= 0) {
        // 第一條路徑時間復雜度為 O(n^2)
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                printf("輸入數據大于等于零\n");
            }
        }
    } else {
        // 第二條路徑時間復雜度為 O(n)
        for(int j = 0; j < n; j++) {
            printf("輸入數據小于零\n");
        }
    }
}

此時時間復雜度為 max(O(n^2), O(n)),即 O(n^2)。

時間復雜度分析的基本策略是:從內向外分析,從最深層開始分析。如果遇到函數調用,要深入函數進行分析。

最后,我們來練習一下

一. 基礎題
求該方法的時間復雜度

void aFunc(int n) {
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            printf("Hello World\n");
        }
    }
}

參考答案:
當 i = 0 時,內循環執行 n 次運算,當 i = 1 時,內循環執行 n - 1 次運算……當 i = n - 1 時,內循環執行 1 次運算。
所以,執行次數 T(n) = n + (n - 1) + (n - 2)……+ 1 = n(n + 1) / 2 = n^2 / 2 + n / 2。
根據上文說的 大O推導法 可以知道,此時時間復雜度為 O(n^2)。

二. 進階題
求該方法的時間復雜度

void aFunc(int n) {
    for (int i = 2; i < n; i++) {
        i *= 2;
        printf("%i\n", i);
    }
}

參考答案:
假設循環次數為 t,則循環條件滿足 2^t < n。
可以得出,執行次數t = log(2)(n),即 T(n) = log(2)(n),可見時間復雜度為 O(log(2)(n)),即 O(log n)。

三. 再次進階
求該方法的時間復雜度

long aFunc(int n) {
    if (n <= 1) {
        return 1;
    } else {
        return aFunc(n - 1) + aFunc(n - 2);
    }
}

參考答案:
顯然運行次數,T(0) = T(1) = 1,同時 T(n) = T(n - 1) + T(n - 2) + 1,這里的 1 是其中的加法算一次執行。
顯然 T(n) = T(n - 1) + T(n - 2) 是一個斐波那契數列,通過歸納證明法可以證明,當 n >= 1 時 T(n) < (5/3)^n,同時當 n > 4 時 T(n) >= (3/2)^n。
所以該方法的時間復雜度可以表示為 O((5/3)^n),簡化后為 O(2^n)。
可見這個方法所需的運行時間是以指數的速度增長的。如果大家感興趣,可以試下分別用 1,10,100 的輸入大小來測試下算法的運行時間,相信大家會感受到時間復雜度的無窮魅力。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容