瞭解排序法是學習演算法的必經之路。所謂排序法,就是將一堆沒有排序過的數字由小到大(或大到小)排列好的演算法。把這些排序法視覺化後就會像以下的這個療癒影片: 大致暸解何謂排序法後,就讓我們先來了解最簡單的排序法:有 O(n²) 時間複雜度的選擇排序法! 時間複雜度為 O(n²) 的演算法,代表著執行步驟會跟著輸入 n 成次方比例的增加。最基礎的排序法之一:選擇排序法(Selection Sort) 是 O(n²) 複雜度的代表。 基本來說,選擇排序只需要重複執行兩個步驟,分別是: 假設有一個 [41, 33, 17, 80, 61, 5, 55] 的陣列,我們用圖的例子來一步一步理解選擇排序是如何進行,在下面的圖中,我們把尚未排序好的數字用紅色標示,這輪找到的最小值以橘色標示,排序好的以藍色標示。 在上面的例子中,我們可以看到每一輪中我們會從還沒排序好的所有數字中找到最小的。 按照這個方式,我們可以在第一輪中找到整個陣列中最小的,第二輪找到第二小的,以此類推,把他們依序放到排序好數列的最後面,就可以確保每一個數字都排序正確。 了解了選擇排序進行的過程後,接下來就輪到分析時間複雜度的部分啦! 觀察選擇排序的時間複雜度,我們同樣把步驟拆成兩個部分討論。
首先我們要先瞭解,從 n 個還沒排序好數字中找到最小值,需要 n 個步驟。 最常見找最小值的方法就是:我們先設陣列的第一個數字是「目前的最小值」,然後往後把陣列的數值一個一個讀取。 如果讀取的下個數比最小值大,就什麼都不用做。而如果讀取到的數比「目前的最小值」小,就把「目前的最小值」換成這個數。重複這個方法把所有陣列裡的數都讀過一遍,就能確保目前的最小值為整個數列的最小值。實際例子如下圖: 知道了找到最小值的步驟後,讓我們觀察一下上面進行的過程。第一個回合要從 7 個數字中找到最小值,需要 7 個步驟。第二個回合則是從 6 個數字中找,需要 6 個步驟,以此類推:如果總共要排序的數字有 n 個,則第一個回合需要 n 個步驟,第二回合需要 n-1 個,一直到最後一個回合需要 1 個步驟為止。 經過神秘數學運算(n+(n-1)+(n-2)+…+1) 後,我們可以得知總共的步驟數等於 n * (n+1) / 2 個步驟。 丟到最左邊丟到最左邊就相對很好理解,每次找到最小的數字時,我就跟「未排序好的數字」中的第一個數字交換位子,並把它標示成已排序好。這樣每一個回合都剛好只需要 1 個步驟,總共 n 個回合,所以需要 n 個步驟。 結合找到最小值與丟到最左邊把上面的兩個結果加起來,(n * (n+1) / 2) + n = n * (n+3) / 2。回想一下我們在這篇提到的,時間複雜度只取最高次方項並省略所有係數,因此雖然選擇排序的步驟數是 n * (n+3) / 2,其時間複雜度我們還是會記為 O(n²)。 選擇排序法在程式碼中的例子,對於程式新手可能需要花比較一點點時間理解。如果你是對程式有一定理解的人,可以嘗試動手實做看看(可以想想要如何實作找最小值,接下來再實作選擇排序就會輕鬆很多)。而如果下方的程式碼對於讀者還有些吃力的話,可以先多多熟悉語法後回來複習即可。 Numbers = [41,33,17,80,61,5,55] O(n²):插入排序法 (Insertion Sort)同樣擁有 O(n²) 時間複雜度,插入排序法 Insertion Sort 則是另外一個非常常見的排序法。簡單來說,插入排序法就是你玩撲克牌時用到的排序法。 讀一個數字
插入合適位置
現在,我們重新使用 [41, 33, 17, 80, 61, 5, 55] 的陣列,在下面的圖中,我們把尚未排序過的數字用紅色標示,這輪要插入的值以橘色標示,排序過的以藍色標示。 從上面可以看到,插入排序法的步驟就像我們玩撲克牌在整牌的時候一樣,只是我們在插入排序法時一次只會插入一個數,而撲克牌在整牌的時候我們有時會同時插入兩三張牌。 接下來,讓我們用慢動作再複習一次: https://commons.wikimedia.org/wiki/File:Insertion-sort-example.gif看完插入排序的慢動作後,讓我們一起來分析它的時間複雜度的部分吧! 插入排序的時間複雜度觀察插入排序法的時間複雜度,我們要同時複習一個在上一篇文章中提到的觀念:
在這邊,我們要用另外一種方法分析時間複雜度:我們分別從排序法會遇到的最佳狀況與最壞狀況來分析。 最佳狀況排序法遇到的最佳狀況會是什麼呢?直觀地想,如果一個陣列在讀取前就剛好已經排序好了,那麼我們在做排序法時,通常可以花比較少的步驟數。 回想插入排序法的兩個步驟,每一輪的「讀一個數字」我們需要一個步驟(不知道為何需要一個步驟可以回去上一篇中複習陣列讀取),而「插入合適位置」則因為每個數字剛好都在合適位置了,所以不需要再做任何動作。 因此,在最佳狀況,每一輪需要一個步驟,總共要做 n 輪,所以時間複雜度是非常單純的 O(n)。 O(n) 的時間複雜度可說是工程師夢寐以求的美妙設計,然而,我們在前面就先警告過大家了,最壞狀況 (Worst Case) 才是我們計算時間複雜度時最關心的。 最壞狀況排序法遇到的最壞狀況會是什麼呢?直觀地想,如果我們要將一個陣列由小到大排列,但陣列在我們排序前剛好是由大到小排列時,我們需要花最多的步驟數才能排列好。 再次回想插入排序法的兩個步驟,每一輪的「讀一個數字」我們同樣需要一個步驟,而「插入合適位置」我們在第一輪需要比較一個(把 61 跟 80 比),第二輪兩個(把 55 跟 80、61 比),第三輪三個,以此類推。 因此,在最壞狀況,我們在「插入合適位置」需要 1+2+3+…+n 個步驟,在「讀一個數字」需要總共 n 個步驟, 經過神秘計算後, 就會得到和選擇排序法相同的 n * (n+3) / 2 個步驟,其時間複雜度我們記為 O(n²)。 插入排序法在程式碼中的例子如下,同樣地,如果下方的程式碼對於讀者還有些吃力的話,可以先多多熟悉語法後回來複習即可。 Numbers = [41,33,17,80,61,5,55] 小結在這篇文章中,我們了解了經典的演算種類:排序法,並認識了最簡單的兩種排序法選擇排序法 (Selection Sort) 與插入排序法 (Insertion Sort)。同樣擁有 O(n) 的時間複雜度,但在分析插入排序法的時間複雜度時,我們分別分析了它的最佳狀況 (Best Case) 與最壞狀況 (Worst Case)。 為了讓讀者也有小小的練習機會,讀者也可以回頭分析選擇排序法的最佳狀況與最壞狀況的分別需要的步驟數,並試看看分析兩者有什麼差別。 在下一篇文章中,我們將從 O(n²) 的排序法加速到 O(n log n),認識更進階的排序法。 想要準時收看新文章嗎?快追蹤 AppWorks School 的粉專與 Medium Publication 吧! 【AppWorks School Batch #12 限時招生中】 Want to Learn More? Weekly I/O!Weekly I/O is a project where I share my learning Input/Output. Every Sunday, I write an email newsletter with five things I discovered and learned that week. Sign up here and let me share a curated list of learning Input/Output with you 🎉 選擇排序法 (Selection Sort)生活中經常要用到排序、分類,例如:
對電腦來說,我們可以將排序問題轉化成以下形式 題目 - 排序輸入說明第1列: 1個整數N,代表接下來有幾個數字。 ( 1 <= N <= 100 ) 第2列: N個待排序的整數 輸出說明將輸出由小到大排序 input
output
處理排序問題有很多方法,以下介紹其中一種適合入門的選擇排序法 概念將數字們分成2類,未排序 和 已排序 一開始所有數字都是未排序 重複 N 次:
第1次可以挑到所有數字中第1小的數字 (最小的數字) 第2次可以挑到所有數字中第2小的數字 ... 第N次可以挑到所有數字中第N小的數字 (最大的數字) 最後就由小到大排完了 實際操作
code
題目練習
延伸閱讀
|