數據結構和算法 | 第一部分第三課:算法複雜度(上)

程序 = 數據結構 + 算法

作者 謝恩銘,公衆號「程序員聯盟」。
轉載請註明出處。
原文:https://www.jianshu.com/p/14982c42b2d8

內容簡介


  1. 算法的正確性
  2. 算法的複雜度
  3. “漸近”度量
  4. 第一部分第四課預告

1. 算法的正確性


上一課 數據結構和算法 | 第一部分第二課:小鴨子們去旅行 中,我們講了一個有趣的小故事,就是爲了引出算法複雜度。

算法複雜度非常重要,要講的內容也很多,所以我們分爲上下兩課。


當程序員需要解決計算機科學相關的問題時,他們(通常)會編寫一個程序。這個程序包含一個實現,也就是說需要把算法用一種編程語言來實現。

我們知道,算法只是對解決問題的步驟的一種精確描述,它並不依賴於程序員所用的編程語言或工作環境。

我們在 數據結構和算法 | 第一部分第一課:什麼是數據結構和算法 裏介紹了一個“煮方便麪”的食譜,這份食譜雖然簡單,但可以說是一個算法。

我們是用中文來描述這份食譜的,如果我現在把這份食譜翻譯成一門外語(比如 英語),但食譜的內容還是不變,只不過換了一種語言來說明罷了。換個英國人來照着這份英文食譜煮方便麪,跟我做出來的會是一樣的。

那麼,當程序員需要把算法用一種編程語言來實現時,需要做什麼呢?

像農夫 Oscar 一樣,他必須首先驗證他的算法是正確的,也就是說它產生了預期的結果,解決了所涉及的問題。這非常重要(如果算法不正確,那我們根本沒有選擇和優化它的必要),有時驗證算法正確性是非常難的一步。

算法正確性,英語是 Algorithm Correctness(algorithm 表示“算法”,correctness 表示“正確性”)。要證明算法的正確性,有許多方法,我們本課程就不詳述了,因爲這不是我們的重點。大家有興趣可以去網上搜索一下。

當然,算法正確了,並不能保證實現這個算法的程序就沒有錯誤(bug)。

一旦有了一個正確的算法,我們用編程語言去實現它(通過編寫一個執行它的程序)。但我們可能會寫出有很多小錯誤的程序,這些小錯誤也許和所使用的編程語言有關,可能在編寫程序的過程中被引入。因爲算法中一般不會描述如何避免犯錯,例如如何管理程序的內存,如何檢查段錯誤(Segmentation Fault),等等,但這些都是程序員實現算法時需要考慮的問題。

2. 算法的複雜度


一旦程序員確信他的算法是正確的,他將嘗試評估其效率,比如他會想知道“這個算法快不快”。

有人可能會認爲,要知道算法快不快的最佳方式是實現該算法並在電腦上進行測試。

有趣的是,通常情況並非如此。例如,如果兩個程序員實現了兩種不同的算法,並在各自的電腦上測試程序運行的快慢。那麼擁有更快速度的電腦的那個程序員可能會錯誤地認爲他的算法更快,但其實並不一定。

而且,要實現一個算法並測試,有時候並不容易。且不說有時候根據一個算法來寫出實現的代碼很難,如果要實現的算法涉及到一枚火箭的發射,難道每次用一枚真的火箭來發射一下去測試算法快不快嗎?我們又不像鋼鐵俠那麼有錢可以任性。

鋼鐵俠

出於這些原因,計算機科學家們發明了一個非常方便而強大的概念,也就是我們接下來要學習的:算法的複雜度

“複雜度”(Complexity,表示“複雜”,是一個名詞。它對應的形容詞是 complex,表示“複雜的”)這個詞有點誤導人,因爲我們這裏不是強調“理解起來有困難”(很複雜,很難),而是指效率(efficiency)。

“複雜度”並不意味着“複雜的程度”。有的算法理解起來很難(很複雜),它的複雜度卻可以非常低。

如果要用一句話來簡單說明算法複雜度,那可以是:
“如果給實現了這個算法的程序一個大小爲 N 的輸入,那麼這個程序將執行的操作的數目的數量級是 N 的一個怎麼樣的函數( f(N) )呢?”

f(N) 中的 f 是 function(函數)的意思,相信以前數學課的時候,大家都學過(比如我們以前很常見的 y = f(x) )。f(N) 表示“N 的函數”。

上面那句話基於以下事實:解決問題的程序取決於問題的起始條件。如果起始條件改變,程序執行的時間也會變長或變短。

複雜度可以量化(通過數學公式)起始條件與算法執行的時間之間的關係。

上面這幾句話乍看有點難以理解。什麼是操作?什麼是操作的數目,什麼是操作的數目的數量級?

不用擔心,我們慢慢講解:

複雜度的學習中會涉及一些數學的概念。所以嘛,學好數學對編程還是很有幫助的,英語同樣很重要。如果你英語和數學比較好,學起編程來會輕鬆很多。可以參看我以前的一篇文章:對於程序員, 爲什麼英語比數學更重要? 如何學習

數量級是指數量的尺度或大小的級別,每個級別之間保持固定的比例。通常採用的比例有 10,2,1000,1024, e(歐拉數,大約等於 2.71828182846 的超越數,即自然對數的底)。
– 摘自 百度百科

爲了“計算操作的數目”,我們必須首先定義什麼是操作。操作其實不難理解,比如第一部分第一課中,我們講了一個“煮方便麪”的算法,其中的步驟是這樣的:

  1. 在鍋子裏倒入適量水
  2. 在爐子上點起火來(如果是電磁爐就不用火)
  3. 把鍋子放在爐子上
  4. 等待水開,轉中火
  5. 把方便麪餅放入鍋中
  6. 煮半分鐘
  7. 放入所有調料包
  8. 煮 1 分鐘
  9. 出鍋

上面這些步驟中的每一步你都可以看成一個操作(operation,“操作”的意思,這個英語單詞也有“運算”的意思)。

但是要計算操作的數目的時候,我們該挑選哪些操作呢? 即使是絕頂聰明的科學家可能也無法非常明確地回答。因爲挑選哪些操作來做計算取決於所考慮的問題(甚至是算法)。

我們必須選擇算法經常執行的一些操作,並且將其作爲度量算法複雜度的基準。

例如,要製作煎雞蛋,可以考慮三種基本操作:

  • 打破雞蛋
  • 鏟碎正在煎的雞蛋
  • 煎熟雞蛋

因此,我們可以統計每份煎雞蛋的菜譜中的雞蛋數目,從而瞭解菜譜(算法)的複雜度(煎雞蛋是一個衆所周知的菜,我們可以預期所有煎雞蛋的菜譜都具有相同的複雜度:對於 N 個雞蛋,我們進行 3N 個操作)。添加鹽、蔥、胡椒或其他佐料的操作非常快,不需要考慮在菜譜(算法)複雜度之內。

煎雞蛋

又例如,在上一課“小鴨子們去旅行”的故事中,裝小鴨子的大箱子是 N 行 N 列,那麼如果農夫 Oscar 採取第一種舊的算法,他須要在池塘和卡車之間進行 N2(N 的平方)趟來回;如果用第二種新的算法,卻只須要 N 趟來回。

這是複雜度的一種很好的度量方式,因爲農夫 Oscar 在池塘和卡車之間來回這個過程是算法的所有操作裏耗時最長的。其他一些操作相對來說不那麼耗時,比如 Oscar 從大箱子裏挑出一隻小鴨子,詢問小鴨子要去哪個池塘,等等。

因此,我們可以說,此算法的總時間幾乎主要花在池塘和卡車之間來回這個操作上了,所以它可以作爲度量此算法的效率的標準。

如果複雜度的概念對你來說還是有點模糊,請不要擔心,之後的實踐課程應該會讓你豁然開朗。

3. “漸近”度量


上面我們介紹了複雜度的基本概念,下面我們繼續講。

我們可以說:複雜度是算法的漸近行爲的度量

漸近的英語是 Asymptotic。那麼,這個有點難以理解的詞“漸近行爲”(或只是“漸進”這個詞)是什麼意思呢?

這意味着“當輸入變得非常大”(甚至“趨於無限”)時。這裏的“輸入”是算法的起始條件的一種量化。

在“小鴨子們去旅行”的故事中,這意味着“當大箱子裏有很多行/列的小鴨子”時,例如 N 是 250。在計算機科學中,“很多”的含義卻略有不同:例如搜索引擎會說“有很多網頁”,1 萬億個網頁…

“當輸入變得非常大”(甚至“趨於無限”)時會有兩種結果:

一方面,恆定的時間(是常量,constant quantity)不予考慮。因爲“恆定的時間”不依賴於輸入。

例如,在農夫 Oscar 開始將小鴨子們帶到每一個池塘去之前,他會先打開卡車的後車門。打開卡車後車門的時間被認爲是“恆定的”:不論他的大箱子裏有 20 行 20 列小鴨子還是有 50 行 50 列小鴨子,開後車門都是花費相同的時間。由於我們要考慮在大箱子的行/列的數目非常大的時候算法的效率,因此與 Oscar 在池塘和卡車之間來回的時間相比,打開卡車後車門的時間可以忽略不計。

另一方面,“常數乘法因子”也不予考慮:複雜度的度量不區分執行 N、2N 或 250N 個操作的算法。爲什麼呢?我們考慮以下兩種取決於 N 的算法:

第一種算法:

做 N 次(操作A)

第二種算法:

做 N 次(操作B 然後 操作C) 

在第一種算法中,我們做 N 次 操作A;在第二種算法中我們做 N 次 操作B,N 次 操作C。假設這兩種算法解決了同樣的問題(所以都是正確的),並且所有操作都被考慮進複雜度的度量中:那麼第一個算法做了 N 個操作,第二個算法做了 2N 個操作。

但我們可以說哪個算法更快嗎?當然不行。因爲這取決於 A、B、C 這 3 個操作所花費的時間:也許 操作B 和 操作C 都比 操作A 快 4 倍,那麼總體來看,操作數爲 2N 的第二種算法比操作數爲 N 的第一種算法反而更快,因爲 1/4 + 1/4 = 1/2(操作B 和 操作C 的耗時都是 操作A 的四分之一)。

因此,不一定對算法的效率具有影響的常數乘法因子在複雜度的度量中不予考慮。

這也使我們能夠回答一開始的那個問題:如果兩個程序員有兩臺計算機,一臺比另一臺速度快 5 倍。5 這個常數乘法因子將被忽略,因此兩位程序員可以毫無問題地比較算法的複雜度。

可以看到,在算法的複雜度的度量中,我們忽略了很多東西,這使得我們能有一個相當簡單和普遍的概念。這種普遍性使複雜度成爲一個有用的工具,但它也有明顯的缺點:在某些非常特殊的情況下,更復雜的算法居然可以用更少的時間來完成(例如,常數因子在實際中也許能扮演非常關鍵的角色:假設農夫 Oscar 的卡車後車門卡住了,他竟花了一整天的時間來打開後車門)。

然而,在絕大多數情況下,複雜度仍是算法性能的可靠指標。特別是當兩個複雜度之間的差距因爲輸入的增大而變得越來越大時。

一種有 N 個比較耗時的操作的算法也許在 N 是 10 或 20 的時候比另一種有 N2(N 的平方)個不那麼耗時的操作的算法運行時間更長;但在 N 是 2萬 或 N 是 5 百萬的時候,複雜度更低的算法將肯定是更快的。

4. 第一部分第四課預告


今天的課有點難度,不妨多讀幾遍。下一課我們繼續研究算法的複雜度。一起加油吧!

下一課:數據結構和算法 | 第一部分第四課:算法複雜度(下)


我是 謝恩銘,公衆號「程序員聯盟」號主,慕課網精英講師 Oscar 老師,終生學習者。
熱愛生活,喜歡游泳,略懂烹飪。
人生格言:「向着標杆直跑」

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