演算法和資料結構-初級 | 第四課:演算法複雜度(下)

程式 = 資料結構 + 演算法
作者 謝恩銘 轉載請註明出處
公眾號「 ofollow,noindex">程式設計師聯盟 」(微信號:ProgrammerLeague )
原文: https://www.jianshu.com/p/3e5e987c7e05
內容簡介
- “大 O”符號
- 時間複雜度和空間複雜度
- 最壞情況下的複雜度
- 第五課預告
1. “大 O”符號
上一課 演算法和資料結構-初級 | 第三課:演算法複雜度(上) 我們開始了演算法複雜度的學習,這一課我們繼續學習後半段。
我們已經看到,複雜度只考慮運算元目的一個數量級(忽略了其他的組分),這是一種近似。
為了表示這種近似,我們使用一個特定的符號,就是著名的大 O 符號。
大 O 符號(Big O notation),又稱為漸進符號,是用於描述函式漸近行為的數學符號。更確切地說,它是用另一個(通常更簡單的)函式來描述一個函式數量級的漸近上界。
在數學中,它一般用來刻畫被截斷的無窮級數尤其是漸近級數的剩餘項。
在電腦科學中,它在分析演算法複雜度的方面非常有用。
大 O 符號是由德國數論學家 保羅·巴赫曼(Paul Bachmann)在其 1892 年的著作《解析數論》(Analytische Zahlentheorie)首先引入的。而這個記號則是在另一位德國數論學家 艾德蒙·朗道(Edmund Landau)的著作中才推廣的,因此它有時又稱為 朗道符號(Landau Notation)。
代表“order of ...”(…階)的大 O,最初是一個大寫希臘字母“Ο”(Omicron),現今用的是大寫拉丁字母“O”。
-- 摘自 百度百科
例如,農夫 Oscar 的第一種演算法有 N 2 個操作,我們就說它的複雜度是 O(N 2 )。類似地,第二種更快的演算法的複雜度是 O(N)。
大 O 符號有點像一個大圓形的袋子,可以把不同的運算元目整合在一起,使之具有一個同樣的數量級。
例如,如果演算法的運算元目分別為 N,5N + 7,和 N / 4,我們都用 O(N) (讀作 “N 的 大 O”。當然,讀法其實不是那麼固定)表示這三個演算法的複雜度。
類似地,如果一個演算法的運算元是(2 * N 2 + 5 * N + 7),那麼它的複雜度是 O(N 2 ):我們忽略了 5 * N 和 7 這兩項,因為它們與 2N 2 相比數量級較小。隨著 N 的增大,這兩項的增長速率比 2N 2 要慢,因此我們保留 2N 2 即可,又因為常數乘法因子不予考慮,因此記為 O(N 2 )。
我們說 f(N) 表示“N 的函式”(例如, f(N) = 2 * N 2 + 5 * N + 7) ),那麼 O(f(N)) 表示的是“大約有 f(N) 個操作的演算法的複雜度”,這邊的“大約”是非常關鍵的。
2. 時間複雜度和空間複雜度
下面我們來學習演算法中常聽到的“時間複雜度”和“空間複雜度”。
為什麼我竟然想到了漫威裡面的大反派滅霸的無限手套呢,上面有時間寶石和空間寶石這兩顆無限寶石。
一定是因為我之前看了《復仇者聯盟3:無限戰爭》(上)的關係...

滅霸的無限手套上的六顆無限寶石
那麼“時間複雜度”和“空間複雜度”這一對“活寶”到底是啥意思呢?且聽我慢慢道來。
“在很久很久以前,宇宙中有 6 顆無限寶石,分別是時間寶石、空間寶石...”
讀者:“小編,你快醒醒,講正經的!”
我:“好,好,講正經的,講正經的~”
為了儘可能精確地表達演算法的複雜度,我們可以做很多選擇。
首先,我們選擇輸入條件的量化。例如通過變數 N(對於 N 行小鴨子,N 個學生,N 架飛機,等)。當然,不一定要用 N 這個變數名,我們可以選擇另一個變數名(比如 M,Z,X,等),但更重要的是我們也可以有不止一個變數。
例如,如果我們的問題是要在一張紙上畫畫,那麼我們可能會將演算法的複雜度表達為畫紙的長度 L 和寬度 W 的函式。同樣地,如果農夫 Oscar 擁有比可用的池塘數目更多的小鴨子的行數,那麼他可以將演算法的複雜度表達為小鴨子的行數 N 和池塘數 P 的函式。
另一個重要的選擇是要度量的操作的型別。到目前為止,我們其實只談論了演算法的效率或效能(就是演算法快不快)。但是,程式設計師不僅對演算法的執行時間感興趣,他們也可能會度量許多其他特性,最常見的是記憶體消耗(Memory Consumption)。
演算法的記憶體消耗也是度量演算法複雜度的標準。例如,如果需要為一個輸入大小為 N 的演算法分配 N 千位元組(KiloByte,一千個位元組,簡稱 KB)的記憶體,則此演算法的記憶體複雜度為 O(N)。
記憶體複雜度是和演算法的記憶體消耗有關的複雜度,度量的並不是演算法的效率,而是消耗/佔用的記憶體空間大小,因此我們把它稱為演算法的空間複雜度(Space Complexity)。
空間複雜度是對一個演算法在執行過程中臨時佔用儲存空間大小的量度,記做 S(n)=O(f(n))。
相對的,演算法的時間複雜度就記為 T(n) = O(f(n))。因為 S 是 Space(空間)的首字母,T 是 Time(時間)的首字母。
在計算演算法的空間複雜度的時候,我們其實也不知道演算法所消耗的具體的記憶體大小(以位元組(Byte)為單位),我們計算的是演算法所使用的(資料)結構的數量級。比如說你使用 N 個大小為 N 的陣列(例如對於小鴨子們去度假的那個故事,可能每隻小鴨子有一個名字,那麼 N 只小鴨子需要 N 個數組來儲存它們的名字,每個數組裡是一隻小鴨子的名字(都是英文字元),而陣列的大小(這裡是字元數)都統一為 N),那麼其空間複雜度為 O(N 2 )。
有些時候,我們需要同時考慮演算法的時間複雜度(執行速度)和空間複雜度(執行期間佔用的記憶體空間的大小)。一般在比較簡單的情況下,我們對演算法的空間複雜度沒有那麼關注。但對於更復雜的問題,演算法的空間複雜度也許會引起更多的重視:例如,我們也許會選擇犧牲一點執行速度來使用更少的記憶體;或者甚至通過增加演算法的空間複雜度來提高執行速度,例如通過在表中儲存已經計算好的結果(快取(cache)的原理)。
對程式的約束越多,所需的資訊就越精確。在電腦科學的某些領域,我們也會對演算法的其他特徵感興趣。而這些特徵中的某些也可以用演算法的某種複雜度來度量。例如,大型計算機或嵌入式系統的程式設計師可能會考慮演算法的功耗,以節省電量。
然而,在一般情況下,我們只關注演算法的時間複雜度和空間複雜度,甚至主要關注時間複雜度。
3. 最壞情況下的複雜度
就如我們之前說過的,演算法執行的運算元很明顯取決於起始條件。
例如,下面是一個非常簡單的演算法,用於獲知一個給定的值是否在值列表中(例如,“我是否已將雞蛋加入我的購物清單?”):
為了獲知一個給定的值是否在值列表中,我們可以這麼做: 遍歷整個列表,在找到給定值的時候即可停下,表示值在列表中; 如果我們已經遍歷完整個列表,仍然沒有找到給定值,那麼說明給定的值不在值列表中。
想象一下,如果我們要查詢的值不在列表中,並且列表裡有 L 個元素。那麼要確定這個值是否存在,演算法就必須遍歷一遍整個列表,將每個值與要查詢的值進行比較,那將需要進行 L 次比較。因此,我們可以說演算法具有 O(L) 的複雜度(很明顯,這裡考慮的是時間複雜度)。我們也可以說,此演算法的時間複雜度是呈線性的(如果我們將輸入列表的大小加倍,那麼此演算法將花費兩倍的時間)。
但是,如果要查詢的值位於列表的最開頭,會怎麼樣呢?
例如,如果“雞蛋”是我們的購物清單中的第一個元素,它會立即被注意到,我們將僅在進行一次操作後就停止搜尋。在其他情況下,即使列表包含 3000 個元素,可能我們的搜尋工作也會在 4 到 5 次操作後停止。
這就是“最壞情況”(Worst Case)的概念發揮作用的地方:在計算演算法的複雜度時,可以認為給定的輸入對於我們的演算法來說是處於“最壞的情況”。我們將計算需要最多操作(而不僅僅是一個或兩個)的輸入情況下的運算元,例如給定值不在列表裡的情況。
從程式設計師的角度來看,這是一種安全性:計算出的複雜度處於“最壞情況”,因此他知道演算法的表現只會更好。
就像網路安全領域的程式設計師會通過自問“最心懷惡意的使用者可能會輸入什麼文字來入侵我的網站?”這樣的問題來敦促自己提升應用程式的安全性一樣,專注於演算法研究的人也想知道“到底是演算法中的哪個元素花了我的演算法的大部分時間?”
這種方法可以度量所謂的“最壞情況下的複雜度”。在本教程中,除非明確指出,我們將只考慮演算法在最壞情況下的複雜度。
4. 第五課預告
終於把演算法複雜度講解得差不多了,真是不容易。大家也辛苦了。
今天的課就到這裡,一起加油吧!
下一課:演算法和資料結構-初級 | 第五課:演算法複雜度實踐
365 天,堅持寫作之 4 / 365,愛上你的每一天!
我是謝恩銘,在巴黎奮鬥的軟體工程師。
熱愛生活,喜歡游泳,略懂烹飪。
人生格言:「向著標杆直跑」