# Chap. 07 - Sort > 參考書目 : Fundamentals of Data Structures in C (2nd), Ellis H., Sartaj S., Susan A.-F. > > 其他科目內容請參見 [[Here]](https://hackmd.io/@ChenZE/By4SOO6Jyl) ## Content * [簡單排序](#簡單排序) * [1. Bubble sort](#1-Bubble-sort) * [2. Selection sort](#2-Selection-sort) * [3. Insertion sort](#3-Insertion-sort) * [4. Shell sort](#4-Shell-sort) * [高效排序](#高效排序) * [1. Merge sort](#1-Merge-sort) * [1.1 Top-down merge sort](#11-Top-down-merge-sort) * [1.2 Bottom-up merge sort](#12-Bottom-up-merge-sort) * [2. Quick sort](#2-Quick-sort) * [3. Heap sort](#3-Heap-sort) * [排序演算法的極限](#排序演算法的極限) * [混合排序](#混合排序) * [1. Tim sort](#1-Tim-sort) * [特殊排序](#特殊排序) * [1. Bucket sort](#1-Bucket-sort) * [2. Counting sort](#2-Counting-sort) * [3. Key-based sort](#3-Key-based-sort) * [4. Radix sort](#4-Radix-sort) ## 簡單排序 ### 1. Bubble sort > The following code is saved in `sorted.c`. ```c= // bubble sort void bubbleSort(int arr[], int n){ int temp; for (int i = 0; i < n-1; i++){ for (int j = 0; j < n-i-1; j++){ if (arr[j] > arr[j+1]){ temp = arr[j+1]; arr[j+1] = arr[j]; arr[j] = temp; } } } } ``` ### 2. Selection sort 最簡單的排序法之一,**每次挑選==最小==(最大)元素放到正確的位置**,過程如下: * 從陣列左側(index=0)開始,搜尋整個 unsorted array 並挑選最小值 * 將最小值與 unsorted array 的左側元素(index=0)交換 * 此時左邊(index=0)會是 sorted array,右邊會是 unsorted array * 從下一個 index 開始繼續上述步驟,直到排序完成 > The following code is saved in `sorted.c`. ```c= // selection sort void selectionSort(int arr[], int n){ int temp; for (int i = 0; i < n - 1; i++){ int minIndex = i; // compare and search the minimum element for (int j = i + 1; j < n; j++){ if (arr[j] < arr[minIndex]){ minIndex = j; } } // swap the minimum and current element temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } } ``` #### Analysis * Time complexity: $O(n^2)$ 因為排序過程中每個位置都需要尋找最小值才能決定 * Space complexity: $O(1)$ * Advantage * Unstable * In-place ### 3. Insertion sort <img src="https://media.geeksforgeeks.org/wp-content/uploads/20240802210251/Insertion-sorting.png" width="600"> (圖片來源:[GeeksforGeeks](https://www.geeksforgeeks.org/insertion-sort-algorithm/)) Insertion sort 的基本概念是將元素**插入到一個已經排序好的 array 之中**並放在**正確的位置**,插入後的陣列依然是**已經排序**好的。 假設有兩個陣列,一個已經排序好(sorted),另一個尚未排序(unsorted)。Insertion sort 的過程就是每次從 unsorted array 中挑選一個數字插入到 sorted array 中的正確位置。 :::info 可以想像成類似卡牌的插入,手上有兩疊卡牌,一疊有排序一疊沒有,從沒有排序的卡牌中每次挑選一張放在已排序卡牌中的正確位置。 ::: Insertion 的**關鍵第一步**是如何在一個**尚未排序的 array 中==選出已經排序好的 sub-array==** * 將 array 的**首項元素 `arr[0]` 視為 sorted array** * 將第 2 個元素 `arr[1]` 與第 1 個元素 `arr[0]` 做**比較(comparison)** * 若 `arr[1] < arr[0]` 則交換(swap)兩者位置 * 若 `arr[1] >= arr[0]` 則不變 * 繼續拿第 3 個元素往前與前 2 個元素**依序做比較插入** :::info 交換(swap)的過程中需要第三方元素來暫存舊的值,最後再換回。以 $A \leftrightarrow B$ 互換為例: * $A \rightarrow X$ * $B \rightarrow A$ * $X \rightarrow B$ 在進行 insertion sort 時因為需要做連續的比較交換,所以可以等**最後確定後再將元素放入正確位置** ::: > The following code is saved in `sorted.c`. ```c= // insertion sort void insertionSort(int arr[], int n){ int i; // i point the bound of sorted array. int j; // j used to compare with key and swap them. int key; // an element which is going to be compared and inserted correct position. for (i = 1; i < n; i++){ key = arr[i]; j = i - 1; while ((j >= 0) && (arr[j] > key)){ arr[j+1] = arr[j]; /** if you want to track the change of input array in each iteration * try these two lines. * arr[j] = key; * printArray(arr, n); */ j = j - 1; } arr[j+1] = key; } } ``` 每一次迭代過程中都要將 `key` 放入正確的位置,所以將 `key` 不斷往前比較 * 比 `key` 大,交換:`arr[j+1] = arr[j];` * 比 `key` 小,正確位置,將 `key` 插入:`arr[j+1] = key;` #### Analysis * Time complexity * Best case 最好的的情況是 array 本身已經排序好,時間複雜度為 $O(n)$ * Worse case 如果 array 是反向的遞減排序(reverse sorted),則每次插入的元素都需要不斷比較交換直到放在第一個,時間複雜度為 $O(n^2)$ * Average case 如果 array 是隨機排序,時間複雜度為 $O(n^2)$ * Space complexity 排序過程不需要額外的空間,空間複雜度為 $O(1)$,是一種 in-place 的排序法。 * Advantage * 對長度較小且快排好的 array 有比較好的效率,適用於長度較小的 array * Stable * In-place :::info 排序演算法的穩定性(stability)指的兩個元素值相同的情況下,排序後的順序與排序前的順序相同。例如考慮以下陣列 $$ [6,\ 2,\ 3^*,\ 4,\ 1,\ 3, 9] $$ 若某個排序演算法執行後的結果是 $$ [1,\ 2,\ 3^*,\ 3,\ 4,\ 6,\ 9] $$ 則該排序演算法具有 stable 的性質,因為 $3^*$ 與 $3$ 的順序相同。反之若結果為 $$ [1,\ 2,\ 3,\ 3^*,\ 4,\ 6,\ 9] $$ 則為 unstable 的排序法 ::: ### 4. Shell sort Shell sort 是基於 insertino sort 的以下性質進行改良的排序演算法: * 對幾乎排好的資料來說 insertion sort 的執行效率高 * 但大部分情況效率很差,因為排序過程中每次只能移動一個元素 為了解決 insertion sort 的第二個缺點,shell sort 採用的是將原始資料切割成小資料,因為小資料可以視為幾乎排序好的陣列,對每個陣列都進行 insertion sort 後可將原始資料排序完成 Step-by-step: * 依照間隔長度 h,每 h 個元素取出來形成 subarray * 以 insertion sort 將每個 subarray 排序(排序後的 subarray 稱為 h-sorted array) * 縮小間隔長度 h 重複第一步 以 [23, 1, 10, 5, 2] 為例,若 h = 2,則 subarray 為 [23, 10, 2] 與 [1, 5] > The following code is saved in `sorted.c`. ```c= //shell sort void shellSort(int arr[], int n){ int h; // control subarray size int i, j; // used to perform insertion sort for (h = n / 2; h > 0; h /= 2){ for (i = h; i < n; i++){ int temp = arr[i]; for (j = i; j >= h && arr[j-h] > temp; j -= h){ arr[j] = arr[j-h]; } arr[j] = temp; } } } ``` ## 高效排序 ### 1. Merge sort 合併排序法(merge sort)是一種利用 **divide and conquer** 的概念所衍生的排序演算法。依照合併方向的不同可以分為兩種作法: * Top-down(recursion ver.) * Bottom-up(iteration ver.) #### 1.1 Top-down merge sort <img src="https://media.geeksforgeeks.org/wp-content/uploads/20230706153706/Merge-Sort-Algorithm-(1).png" width=600> (圖片來源:[GreekforGreek](https://www.geeksforgeeks.org/merge-sort/)) 第一種 merge sort 是**由上而下(top-down)的方向**,是遞迴版本的 merge sort。將長度為 n 的陣列不斷分割成大小相同的兩個子陣列,每個子陣列,將子陣列排序完後再依序合併。 **Step-by-step:** * 使用遞迴的方式不斷**分割輸入陣列**,直到不能分割為止(長度=1) * 從最**小的子陣列開始獨自排序** * 排序完後使用 **merge algorithm 兩兩合併**,直到所有子陣列合併成輸入陣列長度 > The following code is saved in `sorted.c`. ```c= // merge sort void merge(int arr[], int left, int middle, int right, int *mergedArray){ /** merge left and right sub-array and assign to mergedArray * Parameter: * left: index of left point which is beginning of left sub-array * middle: index of middle point which is the partition point also * right: index of right point which is the end of right sub-array */ int i = left; // control left sub-array int j = middle + 1; // control right sub-array int k = left; // control merged list while ((i <= middle) && (j <= right)){ if (arr[i] <= arr [j]){ mergedArray[k++] = arr[i++]; // assign i/k then plus 1 } else{ mergedArray[k++] = arr[j++]; } } // assign remaining sub-array if another done while (i <= middle){ mergedArray[k++] = arr[i++]; } while (j <= right){ mergedArray[k++] = arr[j++]; } // copy to original array for (i=left; i<=right; i++){ arr[i] = mergedArray[i]; } } void topDownMergeSort(int arr[], int left, int right, int *mergedArray){ int middle = left + (right - left) / 2; if (left < right){ topDownMergeSort(arr, left, middle, mergedArray); // divide left sub-array topDownMergeSort(arr, middle+1, right, mergedArray); // divide right sub-array merge(arr, left, middle, right, mergedArray); /** * if you want to show the sorting procedure * add a parameter 'n' which is the lenght of input array 'arr' * then uncomment the following code * printArray(arr, n); // show sorting pdocedure */ } } ``` #### 1.2 Bottom-up merge sort Top-down 的 merge sort 是以遞迴的方式呈現,比較**適合用在小資料**上,若是在大資料上做遞迴函式則會需要較大的 stack space,空間不夠可能會導致**溢位(overflow)** Bottom-up 是 top-down 的反向做法,排序過程**不會涉及陣列的分割(partition)**,因為預設每次的分割長度都是以 $2^n$ 為單位。 **Step-by-step:** 給定一個 size = n 的輸入陣列, * 對於 size = 1 的子陣列可以視為已經排序好的子陣列 * 將所有 size = 1 的子陣列**兩兩合併**,形成 size = 2 的已排序子陣列 * 對於所有 size = 2 的子陣列兩兩合併,形成 size = 4 的已排序子陣列 * 依此類推,直到合併兩個 size = n / 2 的已排序子陣列 > The following code is saved in `sorted.c`. ```c= // merge sort #define MIN(x, y) ((x) < (y) ? (x) : (y)) void bottomUpMergeSort(int arr[], int n, int *mergedArray){ int currSize;// current size of each sub-array int left; // leftmost index of left sub-array // merge sub-arry of size n to create sorted array of size 2*n for (currSize = 1; currSize <= n-1; currSize *= 2){ // pick beginning point of each sub-array of size currSize for (left = 0; left < n-1 ; left += 2 * currSize){ int middle = MIN(left + currSize - 1, n - 1); // end index of left sub-array int right = MIN(left + 2 * currSize - 1, n - 1); // end index of right sub-array merge(arr, left, middle, right, mergedArray); //printArray(arr, n); // if you want to show the sorting process, uncomment this line } } } ``` * **外層**迴圈控制的是 **array size** * 合併的陣列長度不能超過輸入陣列(`currSize = n - 1`) * 每次合併的陣列長度是以**指數形式遞增**(`currSize *= 2`) * **內層**迴圈是計算兩兩**合併過程中陣列的==頭尾==位置** * 左子陣列的起點必須小於倒數第 2 個位置(`left < n - 1`) * 合併後的子陣列長度為 2 * currSize,所以左子陣列的計算必須每次遞增 2 倍長度(`left += 2 * currSize`) * 左子陣列的起點位置必須在 `left + currSuze - 1` 與最後一個元素 `n-1` 做選擇,避免超出輸入陣列的邊界 * 右子陣列的結束位置必須在 `left + 2 * currSize - 1` 與最後一個元素 `n - 1` 做選擇,避免超出輸入陣列的邊界 #### Analysis * Time complexity 兩個方式的時間複雜度都是 $O(n\cdot \log n)$,因為分割過程中樹的高度是 $\log n$ 且每層都有 n 個元素需要做排序。 * Space complexity 不論是 top-down 或 bottom-up 的做法,在排序過程中都需要一個額外的陣列來儲存合併後的已排序陣列(not in-place),所以空間複雜度都是 $O(n)$。但因為遞迴版本會不斷呼叫函式並儲存在 stack 中,因此從記憶體的角度上看,遞迴版本的空間複雜度是 $O(n+\log n)$ * Advantage * Stable 在合併過程中,會優先選擇較小的元素,但當元素大小相同時,會優先選擇放在左子陣列,所以元素會保持輸入時的相對順序,是一種 stable 的排序方法。 * Not inplace ### 2. Quick sort ![image](https://media.geeksforgeeks.org/wp-content/uploads/20240926172924/Heap-Sort-Recursive-Illustration.webp) (圖片來源:[GreekforGreek](https://www.geeksforgeeks.org/quick-sort-algorithm/)) 快速排序法(quick sort)同樣是依據 **divide and conquer** 的精神來進行排序,將要排序的**大陣列依序拆解成許多小陣列**,當小陣列排序好之後,則大陣列也會排序完成。 * 選定一個 **樞紐(pivot)** 並將 array 中的元素依照下列規則**重新排列並分解**成兩個 sub-array * $\le$ pivot 的數放在 pivot 左邊 * $\ge$ pivot 的數放在 pivot 右邊 * 對左右兩個 sub-array 繼續依照上述步驟遞迴分割(partition) * 直到最後的 array 只剩一個元素時就排序完成 (類似 binary search tree 的規則) :::info 做 array partition 的目的是將每次**選定的 pivot 放在正確的位置上** ::: 快速排序法的實作關鍵在於以下兩點:(1) 如何挑選 pivot 與 (2) 如何對 array 做 partition (1) How to choose pivot 一般來說 pivot 會直接挑選每次迭代過程中 array 的首項或末項元素。但如果剛好選到最大或最小值,則樹的高度會 = n。而如果挑到的 pivot 剛好是中位數,則可以形成一個高度為 $\log n$ 的 balance binary tree。 因次為了避免選到最大或最小值,可以每次選擇 3 個數並取三數的中位數做為 pivot (2) How to partition an array 迭代過程中分割 array 的過程有很多種,常見的有 1. Naive partition Naive partition 在分割過程中,會複製整個 array。將較小的元素放在 left sub-array,較大的元素放在 right sub-array。空間複雜度為 $O(n)$ 2. Lomuto partition Lomuto partition 在分割過程中會追蹤較小的元素。從陣列首項開始迭代,當遇到較小(smaller than pivot)的元素就跟當前的元素交換位置 3. **Hoave's partition** Hoave's partition 使用 two pointer 的方式來分割陣列,選定 pivot 之後(通常是陣列第一個元素) * 當左指標元素 > pivot 且右指標元素 < pivot 時交換位置 * 直到左指標 index >= 右指標 index 則完成分割 > The following code is saved in `sorted.c`. ```c= // quick sort void swap(int *a, int *b){ int temp; temp = *a; *a = *b; *b = temp; } int partition(int arr[], int left, int right){ /** Hoare's Partition Algorithm * use two pointers to divide the input array * this method also called Hoare's Partition Algorithm * Parameter: * left: index of the beginning element of arr * right: index of the last element of arr */ int leftPointer = left - 1; int rightPointer = right + 1; int pivot = arr[left]; while (1){ do { // left pointer leftPointer++; } while (arr[leftPointer] < pivot); do { // right pointer rightPointer--; } while (arr[rightPointer] > pivot); if (leftPointer >= rightPointer) return rightPointer; swap(&arr[leftPointer], &arr[rightPointer]); } } void quickSort(int arr[], int left, int right){ /** use Hoare's partition to implement quick sort * Parameter: * left: index of beginning element. * right: index of last element. */ if (left < right){ int partiionIndex = partition(arr, left, right); quickSort(arr, left, partiionIndex); // sort left array quickSort(arr, partiionIndex+1, right); // sort right array } } ``` #### Analysis * Time complexity * Worse case 如上所述,每次在挑選 pivot 時,如果剛好都選到最大或最小,則會形成一個高度 = n 的 skewed tree,時間複雜度為 $O(n^2)$ * Best case 同樣的,如果每此都剛好選到中位數,則會形成一顆高度 = $\log n$ 的 balanced tree,時間複雜度為 $O(n \log n)$ * Average case:$O(n \log n)$ * Space complexity 依照迭代不同,空間複雜度也不同,通常是 $O(\log n)$ 或 $O(n)$ * Advantage * Unstable * Not in-place ### 3. Heap sort 堆積排序(heap sort)是使用 max heap 的結構來實現排序的過程。因此在實際排序之前,必須先將輸入陣列的順序改成 max heap 的結構。 * 將輸入陣列調整成 max heap * 將 root 與 max heap 中的最後元素 last 交換位置,此時 last 的值最大,已經排序完成 * 移除 last * 最後將剩下元素再次調整成 max heap,重回第二步驟 :::info Heap 的陣列表示法中,習慣上會用 1-index 表示根節點的位置來簡化計算。此處因為要進行排序所以使用 0-index 來表示根節點的位置。 ::: 在 heapSort 前半段的目的是要讓輸入陣列形成最大堆積,所以從底層的葉子節點開始往上處理,但因為葉子節點可以視為已經排序好的 heap,所以只要找最後一個非葉節點($\lfloor \frac{n}{2} \rfloor - 1$)的位置,再往上處理即可。 > The following code is saved in `sorted.c`. ```c= // heap sort void heapify(int arr[], int root, int n){ /** rearrange the array(0-index) to form a max heap * Parameter: * arr: input array * root: index of root * n: array length */ int child = 2 * root + 1; // left node by default int rootValue = arr[root]; while (child < n){ if ( (child + 1 < n) && (arr[child] < arr[child + 1])){ child++; } if (arr[child] <= rootValue){ break; } else { arr[root] = arr[child]; root = child; // sink down and check next level child = 2 * root + 1; } } arr[root] = rootValue; } void heapSort(int arr[], int n){ // heapify arr to form max heap for (int i = (n-1)/2; i >= 0; i--){ heapify(arr, i, n); } for (int j = n-1; j > 0; j--){ swap(&arr[0], &arr[j]); // printArray(arr, n); // if you want to show the procedure, uncomment this line heapify(arr, 0, j); } } ``` #### Analysis * Time complexity * Worse/best/average case 的時間複雜度均為 $O(n \log n)$。 * Space complexity * 需看實作的過程,如果使用迴圈實現,則空間複雜度為 $O(1)$,但如果 `heapify()` 使用遞迴方式進行,則空間複雜度為 $O(n \log n)$。 * Advantage * Unstable * In-place ## 排序演算法的極限 以比較和交換兩個動作為主的排序演算法中,假設有 n 個元素的陣列需要排列,則可能的排序情況有 $n!$ 種,以樹狀圖結構的示意圖如下 <img src="https://hackmd.io/_uploads/rJtRyKtr1x.jpg" width=500> 因為有 $n!$ 種可能的排序,所以會有 $n!$ 個葉節點。以高度為 $k$ 的 binary tree 來說,葉節點的個數為 $2^k$,故當葉節點數為 $n!$ 時 $$ n! = 2^k \Rightarrow k = \log_2(n!) $$ 考慮到 0-index 的編號系統,因此樹的高度為 $k+1 = \log_2(n!) + 1$ 因次,以比較和交換為主的排序演算法中,最差的時間複雜度為 $O(n \log n)$ ## 混合排序 ### 1. Tim sort ## 特殊排序 ### 1. Bucket sort 容器排序法(bucket sort)又稱為 bin sort ,是一種特殊排序法,能夠在 best case 的情況下做到 $O(n)$ 的時間複雜度。 Step-by-Step: * 將元素分配到不同的 group/bucket * 對於每個 group/bucket 內部在進行排序,通常是用 insertion sort,因為此時陣列長度變小 * 最後把所有的 group/bucket 依序合併 在進行分組時,會使用類似長方圖找組距的方式決定每一組的長度,再將元素進行映射,轉換到對應的組別。 > THe following code is saved in `sorted.c`. ```c= // bucket sort int getMax(int arr[], int n){ int max = arr[0]; for (int i=0; i<n; i++){ if (arr[i] > max) max = arr[i]; } return max; } int getMin(int arr[], int n){ int min = arr[0]; for (int i=0; i<n; i++){ if (arr[i] < min) min = arr[i]; } return min; } void bucketSort(int arr[], int n){ /** bucket sort * Variable: * bucketCount: setup the bucket number ,if the tange between max and * min is small, then bucketCount can't too large. * buckets: an 2-D array which save all buckets * bucketSize: an 1-D array which save bucket size corresponding to buckets[i] */ int bucketCount = 2; int min = getMin(arr, n); int max = getMax(arr, n); int range = (max - min) / bucketCount; int **buckets = (int **) malloc(bucketCount * sizeof(int *)); // each bucket int *bucketSize = (int *) calloc(bucketCount, sizeof(int *)); // record each bucket size // allocate each bucket size for (int i=0; i<bucketCount; i++){ buckets[i] = (int *) malloc(n * sizeof(int*)); // maximum capacity of each bucket } // scatter the element to each bucket for (int i=0; i<n; i++){ int index = (arr[i] - min) / range; if (index == bucketCount) index -= 1; buckets[index][bucketSize[index]++] = arr[i]; } // sort each bucket for (int i=0; i<bucketCount; i++){ if (bucketSize[i] > 0) insertionSort(buckets[i], bucketSize[i]); } // merge all bucket int index = 0; for (int i=0; i<bucketCount; i++){ for (int j=0; j<bucketSize[i]; j++){ arr[index++] = buckets[i][j]; } free(buckets[i]); } free(buckets); free(bucketSize); } ``` #### Analysis * Worse case: $O(n^2)$ 當 bucket 中的元素分佈不均,某個 bucket 拿到所有元素時就會退化成 insertion sort。但複雜度還是依據使用的排序方式為主。 * Best case: $O(n+k)$ or $O(n)$ 當 bucket 中的元素均勻分佈 * Space complexity: $O(n+k)$ ### 2. Counting sort 計數排序(counting sort) 是一種非比較的排序法,核心思想是計算元素出現的次數進行排序,適合用在輸入數值不大的非負整數陣列中,能夠做到 $O(n)$ 的時間,但需要 $O(n+m)$ 空間,其中 m 是陣列的最大值。 整個過程可以分成 3 個步驟: <img src="https://hackmd.io/_uploads/rJsXTy-8ke.jpg" width=400> 1. 計算出現次數(頻率) * 找到輸入陣列的最大值 m * 建立 size = m + 1 的計數陣列 `countArr` 並計算元素出現的次數 2. 計算計數陣列 `countArr` 的累加和(presum) <img src="https://hackmd.io/_uploads/HJXDTJZL1g.jpg" width=400> 3. 從輸入陣列的末端開始迭代 * 找 `countArr` 中 index = `inputArr[i]` 的元素 * `countArr[index] - 1` 的值就是 `inputArr[i]`` 需要放置的位置 * 更新 `countArr[index]` > The following code is saved in `sorted.c`. ```c= // counting sort void countSort(int arr[], int n){ int max = getMax(arr, n); // compute frequency of each element int *countArr = calloc(max + 1, sizeof(arr[0])); for (int i = 0; i < n; i++) countArr[arr[i]]++; // compute presum of countArr for (int i = 1; i < max + 1; i++) countArr[i] += countArr[i-1]; // sort from last to beginning int *sortedArr = malloc(sizeof(arr[0])*n); for (int i = n - 1; i >= 0; i--) sortedArr[--countArr[arr[i]]] = arr[i]; // copy to original array for (int i = 0; i < n; i++) arr[i] = sortedArr[i]; free(countArr); free(sortedArr); } ``` ### 3. Key-based sort 想像 array 中的元素有多個鍵值 $K^1$, $K^2$, $K^n$可以做排序。以撲克牌為例有分為花色跟點數兩種鍵值 * 鍵值 $K^1$(花色):♠️ > ♥️ > ♦️ > ♣️ * 鍵值 $K^2$(點數):1 > 2 > ... > 13 依據鍵值(key)的重要性進行排序可分為兩種:最重要位優先(Most-Significant-Digit-First, MSD)與最不重要位優先(Least-Significant-Digits-First, LSD) 假設 key 的重要性從 $K^1$ 開始依序遞減,以上述撲克牌為例,對於 MSD 來說: * 先根據 $K^1$ 的值排序成四堆 * 每堆中再依據 $K^2$ 的值排序 對於 LSD 來說: * 先依據 $K^2$ 的值分成 13 堆 * 再依據 $K^1$ 的值使用 stable 的排序法分成四堆 * 最後將每堆結果合併 :::info 不論是 MSD 還是 LSD 都只是描述鍵值的順序,不會涉及排序。 ::: ### 4. Radix sort 基數排序(radix sort)也是一種 key-based 的排序方法,藉由比較每個位數(digit)來達到線性的複雜度。適合用在固定長度的整數(integer)或字串(string)等資料的排序。 因為是多鍵值的排序方法,所以可以依據鍵值的重要性分為 LSD 與 MSD。 假設輸入陣列如下,因為最大數字有三位(digit=3),所以必須迭代三次才能排序完成。 $$ [170,\ 45,\ 75,\ 90,\ 802,\ 84,\ 2,\ 66] $$ 根據個位數(digit=1)的排序結果為: $$ [17 \underline0,\ 9 \underline0,\ 80 \underline2,\ \underline2,\ 2 \underline4,\ 4 \underline5,\ 7 \underline5,\ 6 \underline6] $$ 再根據十位數(digit=2)的排序結果為: $$ [8 \underline02,\ 2,\ \underline24,\ \underline45,\ \underline66,\ 1 \underline70,\ \underline75,\ \underline90] $$ 最後一句百位數(digit=3)的排序結果為: $$ [2,\ 24,\ 45,\ 66,\ 75,\ 90,\ \underline170,\ \underline802] $$ 在每一個 digit 排序的過程中,原則上可以使用任何的排序演算法,只要保證 stability 即可,不過習慣上會使用 counting sort 進行。 > The following code is saved in `sorted.c`. ```c= // radix sort void sortedDigit(int arr[], int n, int digit){ // compute frequency int *countArr = calloc(10, sizeof(arr[0])); // digit from 0 to 9 for (int i = 0; i < n; i++) countArr[(arr[i] / digit) % 10]++; // conpute presum for (int i = 1; i < 10; i++) countArr[i] += countArr[i-1]; // sort int *sortedArr = malloc(n * sizeof(arr[0])); for (int i = n - 1; i >= 0; i--) sortedArr[--countArr[(arr[i] / digit) % 10]] = arr[i]; // copy to original array for (int i = 0; i < n; i++) arr[i] = sortedArr[i]; free(countArr); free(sortedArr); } void radixSort(int arr[], int n){ int max = getMax(arr, n); for (int digit = 1; max / digit > 0; digit *= 10){ sortedDigit(arr, n, digit); /** printArray(arr, n); * if want to show sorting process * uncomment this line */ } } ``` ## Reference * [Rust Algorithm CLub](https://rust-algo.club/index.html) * [GeeksforGeeks](https://www.geeksforgeeks.org) ## Interview Ques. ### 1. Quick sort ```c= #include <stdio.h> void swap(int *, int *); void quickSort(int, int, int []); int main(void) { int arr[] = {1, 5, 4, 3, 2}; int n = sizeof(arr) / sizeof(int); quickSort(0, n-1, arr); printf("arr = "); for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } return 0; } void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } void quickSort(int left, int right, int arr[]) { if (left >= right) { return; } int mid = (left + right) / 2; int pivot = arr[mid]; int L = left; int R = right; while (L <= R) { // find smaller than pivot while (arr[L] < pivot) { L++; } // find greater than pivot while (arr[R] > pivot) { R--; } if (L <= R) { swap(&arr[L], &arr[R]); L++; R--; } } quickSort(left, R, arr); quickSort(L, right, arr); } ```