基本介紹
選擇排序(select sorting)也屬於內部排序法,是從欲排序的資料中,按指定的規則選出來某個元素,再依規定交換位置後達到排序的目的。
它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
基本思想
選擇排序(select sorting)也是一種簡單直觀的排序方法。
基本思想為:
注:n 是陣列大小
- 第一次從
arr[0]~arr[n-1]
中選取最小值,與 arr[0] 交換 - 第二次從
arr[1]~arr[n-1]
中選取最小值,與 arr[1] 交換 - 第 i 次從
arr[i-1]~arr[n-1]
中選取最小值,與 arr[i-1] 交換 - 依次類圖,總共通過
n - 1
次,得到一個按排序碼從小到大排列的有序序列
思路分析
動圖:
說明:
-
選擇排序一共有
陣列大小 - 1
輪排序 -
每 1 輪排序,又是一個迴圈,迴圈的規則
①先假定當前這輪迴圈的第一個數是最小數
②然後和後面每個數進行比較,如果發現有比當前數更小的數,則重新確定最小數,並得到下標
③當遍歷到陣列的最後時,就得到本輪最小的數
④和當前迴圈的第一個數進行交換
程式碼實現
要求:假設有一群牛,顏值分別是 101,34,119,1 ,請使用選中排序從低到高進行排序
演變過程
使用逐步推導的方式,進行講解排序,容易理解。
推導每一步的演變過程,便於理解。
這是一個很重要的思想:
一個演算法:先簡單 --> 做複雜:
就是可以把一個複雜的演算法,拆分成簡單的問題 -> 逐步解決
@Test
public void processDemo() {
int arr[] = {101, 34, 119, 1};
System.out.println("原始陣列:" + Arrays.toString(arr));
processSelectSort(arr);
}
public void processSelectSort(int[] arr) {
// 第 1 輪:
// 原始陣列:101, 34, 119, 1
// 排序後: 1, 34, 119, 101
int min = arr[0]; // 先假定第一個數為最小值
int minIndex = 0;
for (int j = 0 + 1; j < arr.length; j++) {
// 挨個與最小值對比,如果小於,則進行交換
if (min > arr[j]) {
// 如果後面的值比當前的 min 小,則重置min為這個數
min = arr[j];
minIndex = j;
}
}
// 第 1 輪結束後,得到了最小值
// 將這個最小值與 arr[0] 交換
arr[minIndex] = arr[0];
arr[0] = min;
System.out.println("第 1 輪排序後:" + Arrays.toString(arr));
// 第 2 輪
// 當前陣列:1, 34, 119, 101
// 排序後: 1, 34, 119, 101
min = arr[1];
minIndex = 1;
// 第二輪,與第 3 個數開始比起
for (int j = 0 + 2; j < arr.length; j++) {
// 挨個與最小值對比,如果小於,則進行交換
if (min > arr[j]) {
// 如果後面的值比當前的 min 小,則重置min為這個數
min = arr[j];
minIndex = j;
}
}
// 第 2 輪結束後,得到了最小值
// 將這個最小值與 arr[1] 交換
arr[minIndex] = arr[1];
arr[1] = min;
System.out.println("第 2 輪排序後:" + Arrays.toString(arr));
// 第 3 輪
// 當前陣列:1, 34, 119, 101
// 排序後: 1, 34, 101, 119
min = arr[2];
minIndex = 2;
// 第二輪,與第 4 個數開始比起
for (int j = 0 + 3; j < arr.length; j++) {
// 挨個與最小值對比,如果小於,則進行交換
if (min > arr[j]) {
// 如果後面的值比當前的 min 小,則重置min為這個數
min = arr[j];
minIndex = j;
}
}
// 第 3 輪結束後,得到了最小值
// 將這個最小值與 arr[2] 交換
arr[minIndex] = arr[2];
arr[2] = min;
System.out.println("第 3 輪排序後:" + Arrays.toString(arr));
}
測試輸出
原始陣列:[101, 34, 119, 1]
第 1 輪排序後:[1, 34, 119, 101]
第 2 輪排序後:[1, 34, 119, 101]
第 3 輪排序後:[1, 34, 101, 119]
從上述的演變過程來看,發現了規律:迴圈體都是相同的,只是每一輪排序所假定的最小值的下標在遞增。因此可以改寫成如下方式
@Test
public void processDemo2() {
int arr[] = {101, 34, 119, 1};
System.out.println("原始陣列:" + Arrays.toString(arr));
processSelectSort2(arr);
}
public void processSelectSort2(int[] arr) {
// 把之前假定當前最小值的地方,使用變數 i 代替了
// 由於需要 arr.length -1 輪,所以使用外層一個迴圈,就完美的解決了這個需求
for (int i = 0; i < arr.length - 1; i++) {
int min = arr[i]; // 先假定第一個數為最小值
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
// 挨個與最小值對比,如果小於,則進行交換
if (min > arr[j]) {
// 如果後面的值比當前的 min 小,則重置min為這個數
min = arr[j];
minIndex = j;
}
}
// 第 i 輪結束後,得到了最小值
// 將這個最小值與 arr[i] 交換
arr[minIndex] = arr[i];
arr[i] = min;
System.out.println("第 " + (i + 1) + " 輪排序後:" + Arrays.toString(arr));
}
}
測試輸出
原始陣列:[101, 34, 119, 1]
第 1 輪排序後:[1, 34, 119, 101]
第 2 輪排序後:[1, 34, 119, 101]
第 3 輪排序後:[1, 34, 101, 119]
由此可以得到,選擇排序的時間複雜度是 o(n²)
,因為是一個巢狀 for 迴圈
結果是一樣的,但是你會發現,在第 1 輪和第 2 輪的序列是一樣的,但是程式碼中目前也進行了交換,可以優化掉這一個點
優化
public void processSelectSort2(int[] arr) {
// 把之前假定當前最小值的地方,使用變數 i 代替了
// 由於需要 arr.length -1 輪,所以使用外層一個迴圈,就完美的解決了這個需求
for (int i = 0; i < arr.length - 1; i++) {
int min = arr[i]; // 先假定第一個數為最小值
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
// 挨個與最小值對比,如果小於,則進行交換
if (min > arr[j]) {
// 如果後面的值比當前的 min 小,則重置min為這個數
min = arr[j];
minIndex = j;
}
}
// 第 i 輪結束後,得到了最小值
// 將這個最小值與 arr[i] 交換
//但如果minIndex沒有改變,也就是最小值未發生改變,則不需要執行後面的交換了
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
System.out.println("第 " + (i + 1) + " 輪排序後:" + Arrays.toString(arr));
}
}
測試輸出
原始陣列:[101, 34, 119, 1]
第 1 輪排序後:[1, 34, 119, 101]
第 3 輪排序後:[1, 34, 101, 119]
則可以看到,第二輪就跳過了交換這一個步驟,從而優化了這個演算法所要花費的時間。
演算法函式封裝
@Test
public void selectSortTest() {
int arr[] = {101, 34, 119, 1};
System.out.println("升序");
System.out.println("原始陣列:" + Arrays.toString(arr));
selectSort(arr, true);
System.out.println("排序後:" + Arrays.toString(arr));
System.out.println("降序");
System.out.println("原始陣列:" + Arrays.toString(arr));
selectSort(arr, false);
System.out.println("排序後:" + Arrays.toString(arr));
}
/**
* 選擇排序演算法封裝
*
* @param arr 要排序的陣列
* @param asc 升序排列,否則降序
*/
public void selectSort(int[] arr, boolean asc) {
// 把之前假定當前最小值的地方,使用變數 i 代替了
// 由於需要 arr.length -1 輪,所以使用外層一個迴圈,就完美的解決了這個需求
for (int i = 0; i < arr.length - 1; i++) {
int current = arr[i]; // 先假定第一個數為最小值
int currentIndex = i;
for (int j = i + 1; j < arr.length; j++) {
// 挨個與最小值對比,如果小於,則進行交換
if (asc) {
if (current > arr[j]) {
// 如果後面的值比當前的 min 小,則重置min為這個數
current = arr[j];
currentIndex = j;
}
} else {
if (current < arr[j]) {
// 如果後面的值比當前的 min 大,則重置為這個數
current = arr[j];
currentIndex = j;
}
}
}
// 第 i 輪結束後,得到了最小/大值
// 將這個值與 arr[i] 交換
//但如果minIndex沒有改變,也就是最小值未發生改變,則不需要執行後面的交換了
if (currentIndex == i) {
arr[currentIndex] = arr[i];
arr[i] = current;
}
}
}
測試輸出
升序
原始陣列:[101, 34, 119, 1]
排序後:[1, 34, 101, 119]
降序
原始陣列:[1, 34, 101, 119]
排序後:[119, 101, 34, 1]
大量資料耗時測試
排序隨機生成的 8 萬個資料
@Test
public void bulkDataSort() {
int max = 80_000;
int[] arr = new int[max];
for (int i = 0; i < max; i++) {
arr[i] = (int) (Math.random() * 80_000);
}
Instant startTime = Instant.now();
selectSort(arr, true);
// System.out.println(Arrays.toString(arr));
Instant endTime = Instant.now();
System.out.println("共耗時:" + Duration.between(startTime, endTime).toMillis() + " 毫秒");
}
多次執行測試輸出
共耗時:2983 毫秒
共耗時:3022 毫秒
氣泡排序和選擇排序的時間複雜度雖然都是 o(n²)
,但由於氣泡排序每一步有變化都要交換位置,導致了消耗了大量的時間,所以選擇排序相對氣泡排序所花費的時間要更少。
關於氣泡排序請看 資料結構與演算法——排序演算法-氣泡排序