你知道和你不知道的選擇排序

detectiveHLH發表於2019-07-02

1. 什麼是選擇排序?

首先貼上從wiki上弄下來的關於選擇排序的定義。

選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

更加直白的解釋是,每次都從陣列中選出最大或者最小的元素,然後放到陣列的左邊。

2. 選擇排序的過程展示

老規矩,我們還是通過動圖來看一下選擇排序的過程。以下的gif來自於wiki。

你知道和你不知道的選擇排序

然後我們再通過我製作的gif,配上資料再瞭解一下過程。假設我們的待排序陣列還是[5, 1, 3, 7, 6, 2, 4]。

你知道和你不知道的選擇排序

3. 選擇最小值的演算法

我們使用Java來實現最常見的,選擇最小值的選擇排序,其程式碼如下。

private void selectionSort(int[] arr) {
  int min;
  int minIndex;
  for (int i = 0; i < arr.length - 1; i++) {
    min = arr[i];
    minIndex = -1;
    for (int j = i; j < arr.length; j++) {
      if (arr[j] < min) {
        min = arr[j];
        minIndex = j;
      }
    }
    // 排序結束 交換位置
    if (minIndex != -1) {
      exchange(arr, i, minIndex);
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
selectionSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

假設陣列的長度為7,那麼演算法就需要進行6輪。如果陣列的長度為n,則演算法需要進行n - 1輪。

每一輪,演算法都會從剩下的待排序元素中,選出最小的元素,並將其與當前陣列下標為i也就是有序序列的起始位置的元素交換。這樣一來,經過反覆的排序,最終形成有序陣列。

4. 選擇最大值的演算法

上面實現了選擇最小值的程式碼,接下來我們繼續實現選擇最大值的程式碼。

private void selectionSort(int[] arr) {
  int max;
  int maxIndex;

  for (int i = 0; i < arr.length - 1; i++) {
    max = Integer.MIN_VALUE;
    maxIndex = -1;
    for (int j = 0; j < arr.length - i; j++) {
      if (max < arr[j]) {
        max = arr[j];
        maxIndex = j;
      }
    }

    // 排序結束 交換位置
    if (maxIndex != -1) {
      exchange(arr, maxIndex, arr.length - i - 1);
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
selectionSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

這個思想與選擇最小值的演算法完全一樣,只不過是選擇了最大值,每次都將剩餘序列的最大值放到陣列的有序序列的最左邊。

那麼到此,選擇排序最常見的兩種寫法我們都已經實現了。有的兄弟可能會想,這篇部落格是不是結束了。其實我們可以從上面兩個演算法中想到可以優化的點。

既然我們有兩個選擇,一種選擇最小值,另外一種選擇最大值。那麼我們為什麼不同時進行兩個操作呢?

你知道和你不知道的選擇排序

下面我們就來實現這種演算法。

5. 同時選擇最大值和最小值

private void selectionSort(int[] arr) {
  int min;
  int max;
  int minIndex;
  int maxIndex;

  for (int i = 0; i <= arr.length / 2; i++) {
    min = Integer.MAX_VALUE;
    max = Integer.MIN_VALUE;
    minIndex = -1;
    maxIndex = -1;
    for (int j = i; j < arr.length - i; j++) {
      if (arr[j] < min) {
        min = arr[j];
        minIndex = j;
      }
      if (arr[j] > max) {
        max = arr[j];
        maxIndex = j;
      }
    }
    // 排序結束 交換位置
    if (minIndex != -1) {
      if (maxIndex == i) {
        maxIndex = minIndex;
      }
      exchange(arr, i, minIndex);
    }

    if (maxIndex != -1) {
      exchange(arr, maxIndex, arr.length - 1 - i);
    }
  }
}

private void exchange(int arr[], int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};
selectionSort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]

因為選擇最大值和最小值同時進行,相對於上面兩種演算法,同時選擇演算法在執行次數上比前兩種演算法減少了50%。

在執行時間上相對於選擇最小值和最大值分別減少了39.22%和62.20%。

6. 總結

以下是對同一個長度為10000的隨機亂序陣列使用三種演算法的情況。

[0 - 10000] 的亂序陣列 取最小值 取最大值 同時取最大值最小值
100次平均執行時間(ms) 51 82 31
執行次數(次) 50004999 50004999 25005000

最後我們看一下選擇排序演算法的時間複雜度。

  • 最好的情況為O(n ^ 2). 即使整個陣列都是有序的,選擇排序也會執行完選擇最大值或者最小值的過程,只是不會去進行元素交換。
  • 最壞的情況為O(n ^ 2). 同上,會執行完選擇最大值或者最小值的過程,並且每次都需要進行元素交換。

其空間複雜度為O(n),上面三種演算法都屬於原地排序演算法,除了交換元素使用了一個輔助空間之外,沒有額外申請空間,同時選擇排序是不穩定排序。

往期文章:

相關:

  • 微信公眾號: SH的全棧筆記(或直接在新增公眾號介面搜尋微訊號LunhaoHu)
    你知道和你不知道的選擇排序

相關文章