今天我們來學習數組的插入排序算法。
假設你是一個農場工人,收穫了100個蘿蔔,長短不一,農場主讓你從小到大排列好售賣,你應該怎麼做?
一般做法就是選一個蘿蔔,並假設它是最小的,然後依次與其他蘿蔔比較,如果發現更小的,則換成更小的蘿蔔,一直比到最後,然後把作爲最小的蘿蔔放好。接着重新選一個蘿蔔,重複剛纔的操作,直到最後選出作爲第二小的蘿蔔放好,依次重複這個步驟,最後,蘿蔔就從小到大排好了。
從上面的場景中,我們可以發現,如果每次從無序集合中選出一個最小的或者最大的,把選出的這個最小或者最大的放到指定位置,那麼隨着每次選擇的進行,最後我們會得到一個有序的集合,這個過程就是今天的主角——選擇排序的算法過程。
我們來看對數組[6, 3, 9, 7, 8, 5, 0, 2, 4, 1]
的從小到大選擇排序過程:
- 得到一個未排序數組
[6, 3, 9, 7, 8, 5, 0, 2, 4, 1]
。
- 在未排序部分中,選出最小值,此時最小值爲
0
,將0
取出,放到有序部分的下一個位置上,因爲此時沒有有序部分(可以理解爲有序部分最後索引爲-1
),即放到索引爲0
的位置上,但是索引爲0的位置已經有一個元素了,爲了不丟失這個元素,我們交換一下這兩個元素,即將索引爲0
的元素6
與索引爲6
的元素0
交換位置,這樣就將最小值0
放到了指定位置,並且也沒有丟失原來在索引0
的元素6
。
- 繼續在未排序部分中,選出最小值,此時最小值爲
1
,將1
取出,和索引爲1
的元素2
交換位置。
- 繼續在未排序部分中,選出最小值,此時最小值爲
2
,將2
取出,和索引爲2
的元素9
交換位置。
- 繼續在未排序部分中,選出最小值,此時最小值爲
3
,將3
取出,和索引爲3
的元素7
交換位置。
- 繼續在未排序部分中,選出最小值,此時最小值爲
4
,將4
取出,和索引爲4
的元素8
交換位置。
- 繼續在未排序部分中,選出最小值,此時最小值爲
5
,將5
取出,在這一步,我們發現,5
已經處於它所在的位置上,可以不用交換,而且程序想要知道不交換,也需要每次去判斷一下,與其去判斷,不如直接交換,同時還保持了編程實現的簡潔和統一。
- 繼續在未排序部分中,選出最小值,此時最小值爲
6
,將6
取出,同樣,我們還是繼續交換6
。
- 繼續在未排序部分中,選出最小值,此時最小值爲
7
,將7
取出,和索引爲7
的元素9
交換位置。
- 繼續在未排序部分中,選出最小值,此時最小值爲
8
,將8
取出,同樣,我們還是繼續交換8
。
- 繼續在未排序部分中,選出最小值,此時最小值爲
9
,將9
取出,同樣,我們還是繼續交換9
。
- 至此,所有元素就位,排序完畢。
在元素就位的過程中,我們可以發現,由於要將元素放到指定位置,而不能使原來在這個位置上的元素丟失,我們就需要交換元素,而交換元素就導致了相同元素的排列的原始順序被破壞,所以數組的選擇排序是一個不穩定的排序算法。
代碼實現如下:
/**
* 數組的選擇排序算法
* 從小到大排序
*
* @param nums 待排序數組
* @param lo 排序區間lo索引(包含)
* @param hi 排序區間hi索引(不包含)
*/
public static void selectionSort(int[] nums, int lo, int hi) {
// 數組爲null則直接返回
if (nums == null) {
return;
}
// 索引檢查
if (lo < 0 || nums.length <= lo) {
throw new IllegalArgumentException("lo索引必須大於0並且小於數組長度,數組長度:" + nums.length);
}
if (hi < 0 || nums.length < hi) {
throw new IllegalArgumentException("hi索引必須大於0並且小於等於數組長度,數組長度:" + nums.length);
}
if (hi <= lo) {
// lo索引必須小於hi索引(等於也不行,因爲區間是左閉右開,如果等於,區間內元素數量就爲0了)
throw new IllegalArgumentException("lo索引必須小於hi索引");
}
if (lo + 1 >= hi) {
// 區間元素個數最多爲1
// 無需排序
return;
}
// 排序部分待存放元素的索引
int nextSortedIdx = 0;
// 未排序部分的起始位置
int unsortedStartIdx = 0;
while (unsortedStartIdx < hi) {
// 還有元素未排序
// 找到最小值元素所在的索引位置
int minIdx = findMinIdx(nums, unsortedStartIdx, hi);
// 交換排序部分待存放元素和最小值元素
int tmp = nums[minIdx];
nums[minIdx] = nums[nextSortedIdx];
nums[nextSortedIdx] = tmp;
// 排序部分待存放元素的索引+1
nextSortedIdx++;
// 未排序部分的起始位置+1
unsortedStartIdx++;
}
// 無未排序元素,排序完畢
}
/**
* 在[lo, hi)區間內找到最小值元素所在的索引位置
* @param nums 待查找數組
* @param lo 查找區間lo索引(包含)
* @param hi 查找區間hi索引(不包含)
*/
private static int findMinIdx(int[] nums, int lo, int hi) {
// 假設最小值元素所在索引位置爲lo
int minIdx = lo;
for (int i = lo; i < hi; i++) {
if (nums[i] < nums[minIdx]) {
// 如果發現比當前假設元素小的,則更新索引位置
minIdx = i;
}
}
return minIdx;
}
測試代碼如下:
int[] nums = {6, 3, 9, 7, 8, 5, 0, 2, 4, 1};
System.out.println("排序前:" + Arrays.toString(nums));
selectionSort(nums, 0, nums.length);
System.out.println("排序後:" + Arrays.toString(nums));
輸出如下:
排序前:[6, 3, 9, 7, 8, 5, 0, 2, 4, 1]
排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
符合我們的預期。