1. 程式人生 > 實用技巧 >資料結構與演算法——選擇排序

資料結構與演算法——選擇排序

原文連結:https://jiang-hao.com/articles/2020/algorithms-algorithms-selection-sort.html

目錄

排序思想

首先,找到陣列中最小的那個元素,其次,將它和陣列的第一個元素交換位置(如果第一個元素就是最小元素那麼它就和自己交換)。其次,在剩下的元素中找到最小的元素,將它與陣列的第二個元素交換位置。如此往復,直到將整個陣列排序。這種方法我們稱之為選擇排序。選擇排序是一種簡單直觀的排序演算法,無論什麼資料進去都是 O(n²) 的時間複雜度。所以用到它的時候,資料規模越小越好。唯一的好處可能就是不佔用額外的記憶體空間了吧。

那如何選出最小的一個元素呢?

很容易想到:先隨便選一個元素假設它為最小的元素(預設為無序區間第一個元素),然後讓這個元素與無序區間中的每一個元素進行比較,如果遇到比自己小的元素,那更新最小值下標,直到把無序區間遍歷完,那最後的最小值就是這個無序區間的最小值。

演算法效能

選擇排序是不穩定的排序方法。

時間複雜度

選擇排序的交換操作介於 0 和 (n - 1)次之間。選擇排序的比較操作為 n(n - 1)/2 次。選擇排序的賦值操作介於 0 和 3(n - 1) 次之間,1次交換對應三次賦值。

比較次數O(n^2) ,比較次數與關鍵字的初始狀態無關,總的比較次數N=(n-1) + (n-2) + ... +1 = n*(n-1)/2。

交換次數比氣泡排序少多了,由於交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比氣泡排序快。選擇排序每交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對n個元素的表進行排序總共進行至多(n-1)次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。

最好時間複雜度:最好情況是輸入序列已經升序排列,需要比較n*(n-1)/2次,但不需要交換元素,即交換次數為:0;所以最好時間複雜度為О(n²)。

最壞時間複雜度:最壞情況是輸入序列是逆序的,則每一趟都需要交換。即需要比較n(n-1)/2次,元素交換次數為:n-1次。所以最壞時間複雜度還是О(n²)。

平均時間複雜度:О(n²)

空間複雜度:只用到一個臨時變數,所以空間複雜度O(1)

原地操作幾乎是選擇排序的唯一優點,當空間複雜度要求較高時,可以考慮選擇排序;選擇排序實際適用的場合非常罕見。

穩定性

選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩餘元素裡面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那麼,在一趟選擇,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中兩個5的相對前後順序就被破壞了,所以選擇排序是一個不穩定的排序演算法。

程式碼實現

單向選擇

單向選擇的排序演算法也就是最傳統簡單的選擇排序。其Java實現如下:

public static int[] selection_sort_original(int[] nums) {
    //  關鍵效能指標計數
    int loopCnt=0, compareCnt=0, swapCnt=0;
    int[] arr = Arrays.copyOf(nums, nums.length);
    // 總共要經過 N-1 輪比較
    for (int i = 0; i < arr.length-1; i++) {
        loopCnt++;
        int minIndex = i;
        // 每輪需要比較的次數 N-i
        for (int j = i+1; j < arr.length; j++) {
            compareCnt++;
            if (arr[j]<arr[minIndex]) {
                // 遍歷找出每輪剩下元素中最小元素的下標
                minIndex = j;
            }
        }
        // 將找到的最小值和i位置所在的值進行交換
        if (minIndex != i) {
            swapCnt++;
            int tmp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = tmp;
        }
    }
    System.out.println(loopCnt+","+ compareCnt+","+ swapCnt);
    return arr;
 }

雙向選擇

單向選擇方案中的主要思路是,每次遍歷剩餘元素,找出其中最小值,只排定最小值。對於此,有人提出了一種優化方法,即每次遍歷剩餘元素的時候,找出其中最小值和最大值,並排定最小值和最大值,把最大的放到最右邊(降序相反),把最小的放到最左邊(降序相反)。這樣遍歷的次數會減少一半。

public static int[] selection_sort_bidirectional(int[] nums) {
    // 關鍵效能指標計數
    int loopCnt=0, compareCnt=0, swapCnt=0;
    int[] arr = Arrays.copyOf(nums, nums.length);
    int minIndex, maxIndex, tmp;
    for (int left=0, right=arr.length-1; left<right; left++, right--) {
        minIndex = left;
        maxIndex = right;
        loopCnt++;
        for (int i=left; i<=right; i++) {
            compareCnt+=2;
            if (arr[minIndex] > arr[i]) minIndex = i;
            if (arr[maxIndex] < arr[i]) maxIndex = i;
        }
        // 將最小值交換到 left 的位置
        if (minIndex != left) {
            swapCnt++;
            tmp = arr[left];
            arr[left] = arr[minIndex];
            arr[minIndex] = tmp;
        }
        //此處是先排最小值的位置,所以得考慮最大值(arr[max])在最小位置(left)的情況。
        if (left == maxIndex) maxIndex = minIndex;
        // 將最大值交換到 right 的位置
        if (maxIndex != right) {
            swapCnt++;
            tmp = arr[right];
            arr[right] = arr[maxIndex];
            arr[maxIndex] = tmp;
        }
    }
    System.out.println(loopCnt+","+ compareCnt+","+ swapCnt);
    return arr;
}

執行結果

使用資料集https://leetcode-cn.com/submissions/detail/114474973/testcase/測試執行,得結果如下:

49999,1249975000,49983  //單向選擇
25000,1250050000,49987  //雙向選擇

由結果可知,兩種方式除外層迴圈次數雙向比單向少一半之外,在關鍵效能指標(比較次數和交換次數)上並無差異。因此,對於許多人所提出的雙向選擇的排序方式,只能算是選擇排序的一個變種,並無實質上的優化。