## Stable Sort 穩定排序法(stable sorting),如果數值相同,排序後「相對位置與排序前相同」,稱穩定排序。 ## Bubble Sort 氣泡排序的原理是,每回合當前最大的元素都會透過不斷地與其右手邊的元素交換「浮到」它最終的所在位置,從而在進行 n − 1 回合後確定所有元素都已經到 達正確的位置。 ```cpp linenums="1" title="Bubble sort" void bubble_sort() { for(int i = 1; i < n; i++) { for(int j = 0; j < n - 1; j++) { if(a[j] > a[j + 1]) { swap(a[j], a[j+1]); } } } } ``` 時間複雜度 : $O(n^2)$ ### 相關例題 ???+note "[CS Academy - Sorting Steps](https://csacademy.com/contest/archive/task/sorting-steps/statement/) / [USACO Open 2018 Out of Sort S](https://www.luogu.com.cn/problem/P4378)" 給一個長度為 $n$ 陣列 $a_1, \ldots ,a_n$,問 bubble sort 會循環幾次(詳見原題) $n\le 10^5, 1 < a_i < 10^9$ ??? note "思路" 觀察例如範例 `1 3 4 2`,我們是為了要將 2 swap 回原本的位置才需要花那麼久的時間 若每個人左邊的數字都比他小的話,就完成排序了,不然就需要將左邊比他大的數字移到右邊去 每個回合會把一個(若存在)自己左邊比自己大的數字移動到自己右邊,當每個人都把比自己大的數字移動到自己右邊,就完成排序了 ???+note "[USACO 2018 OPEN Out of Sorts G](http://www.usaco.org/index.php?page=viewproblem2&cpid=837)" 給一個長度為 $n$ 陣列 $a_1, \ldots ,a_n$,問雙向 bubble sort 會循環幾次(詳見原題) ??? note "思路" 觀察範例測資 : ``` 1 8 5 3 2 1 5 3 2 8 1 2 5 3 8 ``` 可以發現其實就是 swap(2, 8) 而已,有了這個觀察後,我們考慮至少要換幾次,對於一個「分界線」,我們需要將不該在左邊的數字 swap 到右邊去,這個數量也相當於不該在右邊的數字數量,因為對於一條分界線每次至多只能有一組交換,所以令 $m_i=$ 分界線 $(i, i+1)$ ,不該在左邊的數字數量,可以發現我們「至少要換」 $m_i$ 次,對於每個分界線的 lower bound,也就是 $m_i$,max 起來就會是我們需要循環的次數。證明的部分,跑了 $\max(m_i)$ 輪後若還未 sorted,代表存在逆序對,但實際上每條分界線左右都是合法的,所以序列已 sorted。 實作上不該出現在左邊的數字就是離散化後,在 $i$ 左邊比 $i$ 大的數字數量,可以用 BIT 維護。 ## Selection sort 選擇排序法的原理是把要排序的序列分成兩堆,一堆是由原序列最小的前 k 個元素所組成並且已經照大小排列,另一堆則是原序列中剩餘的 n − k 個尚未排序的元素。算法每回合都會從未排序堆中選出最小的元素,然後將其移動到已排序堆中的最後面,類似根據大小一個一個叫號排隊,n − 1 回合後便把原序列給排列好了。 ```cpp linenums="1" title="Selection sort" void selection_sort() { for (int i = 0; i < n - 1; i++) { int min_idx = i; for (int j = i + 1; j < n; j++) { if (a[j] < a[min_idx]) min_idx = j; } swap(a[i], a[min_idx]); } } ``` 時間複雜度 : $O(n^2)$ ## Insertion sort 該算法一樣把原序列區分成已排序和未排序的兩堆,接著把未排序的元素一個一個插入到已排序堆中正確的位置。 ```cpp linenums="1" title="Insertion sort" void insertion_sort() { for (int i = 1; i < n; i++) { int j, tmp = a[i]; for (j = i - 1; j >= 0 && a[j] > tmp; j--) { a[j + 1] = a[j]; } a[j + 1] = tmp; } } ``` 時間複雜度 : $O(n^2)$ ## Merge Sort 合併排序法的原理是把要排序的序列分成前後兩等份,分別遞迴處理成兩個排序好的序列後,再將這兩個序列合併成一整個排序好的序列。此演算法需要額外 O(n) 的空間 時間複雜度 : $O(n \log n)$ ## Quick sort 合併排序法的原理是選擇序列中一個元素做為基準 (pivot),接著將小於基準的元素放到序列左邊,大於基準的元素放到序列右邊,接著遞迴處理左右兩個新的序列。快速排序法平均時間複雜度為 $O(n \log n)$,但在基準選得不好,導致左右兩序列大小差很多的情況下,可能達到 $O(n^2)$ 的複雜度。一般為了避免這種情況,基準的選擇會是隨機的。快速排序法的優點是不需要額外記憶體。 期望 : $O(n \log n)$,最差 : $O(n^2)$ ## Counting sort ## Radix sort Radix sort 適用於整數,由個位數開始,對第 $i$ 位數都做一輪排序。每一輪排序只看第 $i$ 位數的大小,以 10 進位整數來說,第 $i$ 位數只有 10 種可能,於是就使用 10 個桶子,並將序列中的數字依照第 $i$ 位數丟進相對應的桶子,再由 $0$ 號桶子開始,將每個桶子中的數字依照放入的順序拿出,replace 原本的序列。由低位數一直做到最高位數,做完後就完成排序了。最多會進行 $\log_{10} C$ 輪排序,每輪排序為 $O(n)$,因此總時間複雜度為 $O(n \log_{10} C)$。一般我們會將 $\log_{10} C$ 視為常數,如排序 int 時該數為 $10$,因此 Radix sort 可以視為一個線性的排序方法。 Radix sort 相較於 Counting sort 可以處理範圍較大的數據。 ```cpp linenums="1" title="radix sort" void radix_sort() { vector<int> bucket[10]; int radix = 1; for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) { bucket[i].clear(); } for (int i = 0; i < n; i++) { int digit = (a[i] / radix) % 10; bucket[digit].push_back(a[i]); } int pos = 0; for (int i = 0; i < 10; i++) { for (auto num : bucket) { a[pos++] = num; } } radix *= 10; } } ```