今天我们来学习数组的插入排序算法。
假设你是一个农场工人,收获了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]
符合我们的预期。