1. 程式人生 > >初學者學演演算法|談什麼是演演算法和時間複雜度

初學者學演演算法|談什麼是演演算法和時間複雜度

進階篇:深入時間複雜度的實際情況

到目前為止,我們認識了何謂演演算法,以及評斷演演算法設計好壞的工具:時間複雜度。

接下來的部分,屬於比較進階的練功,對剛接觸的新手可能會覺得有點難度或沒辦法完全理解。小小的建議是,如果你看第一次沒辦法完全體會,可以留到對程式設計更熟悉後再回頭複習。而當然,如果有任何問題也都歡迎討論!

接下來,就讓我們來看一下進階篇吧!

步驟次數

細心的讀者可能會好奇,為何上面比較圖的縱軸專案是步驟數而不是執行時間呢?

首先,我們要先來小小定義一下在程式設計中的「步驟次數」是指什麼。通常在程式碼中,每個動作只要被執行一次,我們就會說他是一個步驟,而這些動作包含印出、賦值、陣列讀取等等。

舉例來說,在程式碼中我們宣告:

x = 10

賦予變數 x 數值 10 的這個動作就是一個步驟。再舉另外一個例子:

x = 10for i from 1 to x:    print("你好!")

在 for 後面的迴圈中,輸出”你好!”這個動作被執行從 x = 1 到 x = 10 總共 10 次,因此這個 for 迴圈總共的步驟次數就是 10。但也別忘了,第一行 x = 10 也是一個步驟,所以上面的三行程式碼總共的步驟次數 = 11。

執行時間 vs 步驟次數

現在,我們瞭解了步驟次數後,我們終於可以再進一步瞭解一個重要觀念:

演演算法有多快不是以秒衡量,而是以步驟次數

上面看電影的例子為了方便釐清大 O 符號跟輸入 n 的關係,我們用時間來舉例,但實際上,我們要真正衡量演演算法時,是以步驟次數來看。

為何看演演算法有多快不是用執行時間要幾秒呢?想像一下,美國 NASA 超級電腦需要執行 1 秒的程式,拿到我們家裡電腦執行可能需要 100 秒。而同樣的程式,用 C 語言寫跟用 Python 寫也跑的不一樣快。因為電腦效能和語言特性的都會造成影響,因此用執行時間來衡量演演算法的快慢顯然不是個穩定的指標。

回過頭來使用上面看電影的例子,假設取得電影的動作我們可以用一個「只要一個步驟」的函式 get_movie( ) 替代,則「想看十部電影之走路方案」的程式碼就是單純的

// 想看十部電影之走路方案:步驟次數 = 1 次// n = 10
get_movie( )

而「想看十部電影之下載方案」的程式碼可以寫成

// 想看十部電影之下載方案:步驟次數 = 10 次// n = 10
for i from 1 to n:      get_movie()

我們從上面兩段程式碼可以觀察,所謂「步驟次數」通常會以一段程式碼中迴圈執行的次數決定(先不考慮遞迴狀況,因為這部分太複雜)。假如今天是從 1 迭代到 n 的迴圈,代表步驟次數 n。同樣的如果是一個 n 迴圈裡面再包一個 n 迴圈,代表他的步驟次數就是 n²。而如果整段程式碼都沒有用到迴圈或遞迴,則步驟次數通常以 1 表示。

實務上我們如何記錄時間複雜度

好奇的讀者可能會有疑問,那如果一個程式中有好幾個迴圈,有單純的迴圈也有雙重迴圈,這樣的步驟次數換成大 O 符號要怎麼表示呢?讓我們從下面的程式碼來瞧瞧:

for i from 1 to n:      get_movie()
for i from 1 to n:      for j from 1 to n:           get_movie()
for i from 1 to n:      for j from 1 to n:           get_movie()

第一段的迴圈步驟次數是 n,第二段的步驟次數是 n²,第三段的步驟次數又重複了一次 n²,所以直觀地想,這一大段的程式碼的步驟次數就是 2n² + n。

但實務上,當我們要將步驟次數轉換成以大 O 符號紀錄的時間複雜度時,有一個很重要的原則,也就是要「盡可能化簡」。

數學上,在 n 相當相當大的時候,我們在比較兩個數的大小時可以只比較他們的最高次方。因此在記錄時間複雜度時,我們同樣地會只記錄最高次方的那一項。

另外,為了方便記錄,我們也會省略所有係數,讓時間複雜度的記錄可以符合「盡可能化簡」的原則。也因此,上面有 2n² + n 個步驟的程式,我們會把它的時間複雜度簡單記成 O(n²)。

此時,也許細心的讀者會有疑問,為何 2n² + n 個步驟的程式,我們會將它的時間複雜度記的跟兩倍步驟數( 4n² + 2n )一樣,都是 O(n²) 呢?

這邊,我們可以把大 O 符號想像成一個區段類別,而不是一個非常非常精確的記錄單位。因此所有落在最大係數 n² 這個區段的步驟數,我們就會全部將它們歸在 O(n²) 囉!

結論

呼!終於結束看完了這篇文章,希望身為程式麻瓜的讀者還保持清醒,而有學過程式的讀者也能藉這篇文章複習一下。

如果對演演算法與時間複雜度還意猶未盡,敬請鎖定 AppWorks School 的 Medium,我們在接下來的文章中會透過六個不一樣的時間複雜度,去認識在程式設計中一定要知道的六種經典演演算法哦!最後,留下這篇文章的小小筆記,讓大家方便溫故知新!

溫故知新

  1. 演演算法的簡單定義:輸入 +演演算法 = 輸出
  2. 時間複雜度:衡量演演算法執行好壞的工具
  3. 大 O 符號:用來描述演演算法在輸入 n 個東西時,總執行時間與 n 的關係
  4. 在 n 非常大時,好的演演算法設計可以省下非常多時間
  5. 演演算法的速度不是以秒計算,而是以步驟次數
  6. 實務上,我們只會紀錄最高次方的那一項,並忽略其所有的係數

延伸閱讀

如果對程式已經有一定基礎,想要更瞭解演演算法去增程序式設計的技巧的話,可以參考下面列出的這個網站,有非常非常詳盡的演演算法介紹。

註:事實上,表示時間複雜度的嚴謹符號(數學上稱為漸進符號)有五個,分別是 O、Ω、Θ、o、ω。而實務上大部分指的大 O 符號 (唸作大歐),意思其實比較相近於大 Θ (唸作 theta),但因為方便以及一些習慣大部分人還是使用大 O 比較多。為了避免初學者增加額外的負擔以及看到嚴謹數學就頭痛的壓力,本篇還是以大 O 來介紹,漸進符號的較嚴謹數學定義可見:http://alrightchiu.github.io/SecondRound/complexityasymptotic-notationjian-jin-fu-hao.html#com
簡單來說,O(大歐)代表的是「演演算法執行步驟數目上限」,Ω(大歐美加 Omega)代表的是下限,而 Θ(大細塔)則代表演演算法執行步驟數目同時滿足的上限與下限(不多不少剛剛好)。