今天來學習數組的插入排序算法。
在講算法之前,我們先來想象這樣一個場景:我們要上體育課,體育老師要求同學們從高到矮排好隊。當同學們都排好隊的時候,小甲同學遲到了。這時候小甲同學隨便站個地方可不行,因爲這樣隊伍就不是排好的了,所以要將小甲同學插入到隊伍中,使隊伍重新又是從高到矮排好的。小甲同學對比自己和同學們的身高,找到自己的位置,排到隊伍裏,就完成了隊伍的排序。
這個將一個無序元素插入到n個元素的有序集合,使新的(n+1)個元素的集合變成重新有序的動作就是插入排序的基本思想。藉助這種思想,我們就可以把一個數組分成有序的和無序的兩部分,不斷將無序的部分裏面的元素取出來插入到有序的部分中,使數組最終變爲有序的。因爲一個元素本身就有序了,我們可以把第一個元素作爲最初的有序部分。
我們來看對數組[6, 3, 9, 7, 8, 5, 0, 2, 4, 1]
的從小到大排序過程:
- 得到一個未排序數組
[6, 3, 9, 7, 8, 5, 0, 2, 4, 1]
。
- 得到首元素
6
,因爲只有一個元素,所以直接放入有序部分。
- 取出下一個元素
3
,將其依次與有序部分的元素對比,此時有序部分元素只有6
,對比後發現3
小於6
,所以3
要作爲首元素插入,得到新的有序部分[3, 6]
。
- 取出下一個元素
9
,將其依次與有序部分的元素對比,發現9
比有序部分的最後一個元素6
還要大,所以將9
插入到有序部分的末尾,得到新的有序部分[3, 6, 9]
。
- 取出下一個元素
7
,將其依次與有序部分的元素對比,發現7
比9
小,比6
大,所以將7
插入到6
和9
之間,得到新的有序部分[3, 6, 7, 9]
。
- 取出下一個元素
8
,將其依次與有序部分的元素對比,發現8
比9
小,比7
大,所以將8
插入到7
和9
之間,得到新的有序部分[3, 6, 7, 8, 9]
。
- 取出下一個元素
5
,將其依次與有序部分的元素對比,發現5
比6
小,比3
大,所以將5
插入到3
和6
之間,得到新的有序部分[3, 5, 6, 7, 8, 9]
。 - 取出下一個元素
0
,將其依次與有序部分的元素對比,發現0
比有序部分的首元素3
還要小,所以將0
作爲有序部分的首元素插入,得到新的有序部分[0, 3, 5, 6, 7, 8, 9]
。
- 取出下一個元素
2
,將其依次與有序部分的元素對比,發現2
比3
小,比0
大,所以將2
插入到0
和3
之間,得到新的有序部分[0, 2, 3, 5, 6, 7, 8, 9]
。
- 取出下一個元素
4
,將其依次與有序部分的元素對比,發現4
比5
小,比3
大,所以將4
插入到3
和5
之間,得到新的有序部分[0, 2, 3, 4, 5, 6, 7, 8, 9]
。
- 取出最後一個元素
1
,將其依次與有序部分的元素對比,發現1
比2
小,比0
大,所以將1
插入到0
和2
之間,得到新的有序部分[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
。
- 至此,所有元素歸入有序部分,排序完成。
算法實現代碼如下:
/**
* 數組的插入排序算法
* 從小到大排序
*
* @param nums 待排序數組
* @param lo 排序區間lo索引(包含)
* @param hi 排序區間hi索引(不包含)
*/
public static void insertionSort(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;
}
for (int i = lo + 1; i < hi; i++) {
// 獲取當前元素
int e = nums[i];
// 從當前位置的前一個元素開始,往前查找,直到索引爲-1
// 或找到第一個小於等於當前元素的值(等於時可保證穩定排序)
int j = i - 1;
for (; j >= lo; j--) {
if (nums[j] <= e) {
// 如果找到一個元素小於等於當前元素
// 退出循環
break;
}
}
// 此時j索引表示當前元素插入位置的上一個位置
// 所以插入位置索引爲j+1
// j可能爲-1,此方法仍然適用
int insertIdx = j + 1;
// 如果insertIdx == i,說明此時元素已經就位了,無需插入
if (insertIdx < i) {
// 元素未就位,需要插入
// 插入方法爲從insertIdx(包含)到i(不包含)之間的元素往後移一個位置,
// 當前元素放到insertIdx的位置上
System.arraycopy(nums, insertIdx, nums, insertIdx + 1, i - insertIdx);
nums[insertIdx] = e;
}
}
}
測試代碼如下:
int[] nums = {6, 3, 9, 7, 8, 5, 0, 2, 4, 1};
System.out.println("排序前:" + Arrays.toString(nums));
insertionSort(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]
符合我們的預期。