編碼 · 演算法
選擇排序法教學:原理、程式碼與時間複雜度
簡答
選擇排序法把串列分成左邊已排序的部分與右邊未排序的部分。每一步,它在未排序的部分找出最小的元素,並交換到第一個未排序的位置。它永遠要做約 n²/2 次比較,所以在任何情況下都是 O(n²),但最多只做 n − 1 次交換,額外空間為 O(1)。常見的陣列版本不是穩定排序。
開啟行排序用白話說明原理
掃描整個串列,找出最小的元素。把它與索引 0 的元素交換;現在索引 0 就是最終位置。接著從索引 1 開始掃描串列其餘部分,找出最小的並交換到索引 1。持續進行,直到只剩一個元素,那個元素必然是最大的。
與泡沫排序和插入排序不同,選擇排序不會因為輸入已排序而受益:它仍然得掃描每個剩餘元素,才能確定找到了最小值。
逐步範例
將 [64, 25, 12, 22, 11] 排序。直線之前的部分已排序並為最終位置。
| 步驟 | 未排序的部分 | 找到的最小值 | 交換 | 該步驟後的串列 |
|---|---|---|---|---|
| 1 | 64, 25, 12, 22, 11 | 索引 4 的 11 | 索引 0 ↔ 4 | [11 | 25, 12, 22, 64] |
| 2 | 25, 12, 22, 64 | 索引 2 的 12 | 索引 1 ↔ 2 | [11, 12 | 25, 22, 64] |
| 3 | 25, 22, 64 | 索引 3 的 22 | 索引 2 ↔ 3 | [11, 12, 22 | 25, 64] |
| 4 | 25, 64 | 索引 3 的 25 | 不需要交換 | [11, 12, 22, 25 | 64] |
五個元素共四步,共 4 + 3 + 2 + 1 = 10 次比較,以及三次實際的交換。
Python 實作
def selection_sort(items):
"""Sort a list in place, ascending. Returns the same list."""
n = len(items)
for i in range(n - 1):
smallest = i
for j in range(i + 1, n):
if items[j] < items[smallest]:
smallest = j
if smallest != i:
items[i], items[smallest] = items[smallest], items[i]
return items
print(selection_sort([64, 25, 12, 22, 11])) # [11, 12, 22, 25, 64]
print(selection_sort([2, 1])) # [1, 2]外層迴圈執行 n − 1 次,因為最後剩下的那個元素自然就在正確位置。內層迴圈只記錄索引;交換在每個外層步驟只發生一次。
時間與空間複雜度
| 指標 | 數值 | 原因 |
|---|---|---|
| 比較次數(任何情況) | n(n − 1)/2 | 每一步都掃描整個未排序的部分 |
| 時間(最佳/平均/最差) | O(n²) | 掃描與資料順序無關 |
| 交換次數(最差) | n − 1 | 每一步至多交換一次 |
| 額外空間 | O(1) | 原地排序,只有一個索引變數 |
寫入次數少是選擇排序唯一真正的優點。如果寫入記憶體比讀取昂貴得多(例如某些快閃記憶體,或搬移大型記錄時),減少寫入就可能很重要。循環排序(cycle sort)更進一步,每個元素至多只寫入一次。
穩定性:為什麼選擇排序不穩定
穩定排序會讓相等的元素保持原本的相對順序。選擇排序中的長距離交換,可能讓某個元素跳過另一個與它相等的元素。
def selection_sort_by_key(items, key):
n = len(items)
for i in range(n - 1):
m = min(range(i, n), key=lambda j: key(items[j]))
items[i], items[m] = items[m], items[i]
return items
cards = [("4", "hearts"), ("4", "spades"), ("2", "clubs")]
print(selection_sort_by_key(cards, key=lambda c: c[0]))
# [('2', 'clubs'), ('4', 'spades'), ('4', 'hearts')] order of the 4s changed2 與紅心 4 交換,使紅心 4 跑到黑桃 4 的後面。穩定版本也是有的:不用交換,而是移除最小值並插入到位置 i,其餘元素右移。這樣能保持順序,但每一步要花 O(n) 次寫入。
選擇排序與其他排序法的比較
| 演算法 | 最佳 | 平均 | 最差 | 額外空間 | 穩定 |
|---|---|---|---|---|---|
| 選擇排序 | O(n²) | O(n²) | O(n²) | O(1) | 否 |
| 泡沫排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 堆積排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 否 |
| 合併排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 是 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 否 |
堆積排序其實是用了更好資料結構的選擇排序:它同樣反覆選出剩餘元素中最大的,但用二元堆積在 O(log n) 內找到,而不是 O(n) 的掃描,使總時間降為 O(n log n)。
何時使用選擇排序
實際的 Python 程式請使用內建的 sorted() 或 list.sort()。選擇排序值得了解的場合包括:作為教學範例、在受限環境中處理大小固定的極小陣列,以及寫入次數必須降到最低時。它也是常見的考試與面試題,通常的問法是:追蹤每個步驟、計算比較與交換次數,並解釋為何它不穩定。
常見問題
選擇排序是穩定的嗎?
標準的原地陣列版本不穩定,因為長距離的交換可能讓元素越過與它相等的元素。改用插入而非交換的版本是穩定的,但寫入次數較多。
選擇排序的時間複雜度是多少?
最佳、平均與最差情況都是 O(n²)。無論輸入順序如何,它永遠做 n(n − 1)/2 次比較。
選擇排序會做幾次交換?
最多 n − 1 次,每個外層步驟一次。這比泡沫排序或插入排序少,後兩者可能做出 O(n²) 次交換或位移。
選擇排序和插入排序有什麼不同?
選擇排序從未排序部分挑出最小值,接到已排序部分的後面。插入排序則取下一個元素,插入到已排序部分的正確位置。插入排序是穩定的,最佳情況為 O(n);選擇排序兩者皆不是。