插入排序
我们先抛开插入排序的定义和实现,从一个简单例子:插入一个元素到有序数组进行展开说明。
插入一个元素到有序数组
假设我们现在有一个有序数组[1,3,5],现在给你一个数字2,假定这个有序数组容量足够的情况下,如何将数字2插入到这个有序数组呢?一般我们可以得出如下的粗略算法步骤:
- 寻找2在有序数组中的下标位置。
- 将要插入的下标位置的元素及之后的元素往后移,为2腾出空间。
- 将2插入到数组的下标位置。
现在我们一个个分析这三个步骤的实现
查找插入的位置
查找元素要插入的数组的下标位置,我们有两种实现方式,一种是从头到尾,另一种是从尾到头。从头到尾的查找,大于2的数字就是要插入的位置,从尾到头查找,小于2的数字的后一个元素下标就是要插入的位置,代码如下:
// 从头到尾查找
int result = 0;
// 最后一位是占位符,我们不需要查找
for (int i = 0; i < sortedArray.length - 1; i++) {
if (sortedArray[i] > 2) {
result = i;
break;
}
}
System.out.println("result:" + result);
result = 0;
// 从尾到头查找
// 最后一位是占位符,我们不需要查找
for (int i = sortedArray.length - 2; i >= 0; i--) {
if (sortedArray[i] < 2) {
result = i + 1;
break;
}
}
System.out.println("result:" + result);
元素后移
因为我们已经得到了要插入元素的下标,所以我们需要将要插入的下标位置元素及之后的元素往后移,为2腾出空间。那我们该怎么实现呢?因为我们预留了占位符,所以我们不需要额外的申请新的数组进行拷贝来实现,我们可以从数组的尾到要插入元素的下标之间进行比较交换就可以达到,代码如下:
// 从数组尾到要插入元素下标,将数组往后移动元素
for (int j = sortedArray.length - 1; j > result; j--){
CommonUtils.swap(sortedArray, j - 1, j);
}
CommonUtils.printArray(sortedArray);
插入元素
插入元素就简单多了,因为已经查找到要插入元素的数组下标,所以直接使用数组赋值就可以了,代码如下:
sortedArray[result] = 2;
插入一个元素到有序的数组完整实现
我们上诉代码实现时,使用了一个占位符来防止数组的下标越界问题,这是一个愚蠢的演示代码,我们最终的代码实现需要申请一个长度大于1个有序数组的新数组来存放有序数组和要插入的元素,因此代码实现如下:
private static int[] insertOneElementIntoSortedArray(int[] sortedArray, int insertElement) {
assert sortedArray != null;
final int len = sortedArray.length;
// 申请一个长度大于1个有序数组的新数组来存放有序数组和要插入的元素
int[] result = new int[len + 1];
// 将有序数组复制到新数组中
for (int i = 0; i < len; i++) {
result[i] = sortedArray[i];
}
// 将插入的元素放置到新数组末尾,便于比较和交换
result[len] = insertElement;
// 1. 查找插入元素的下标位置,从尾到头开始查找
int insertIndex = 0;
for (int i = len - 1; i >= 0; i--) {
if (sortedArray[i] < insertElement) {
insertIndex = i + 1;
break;
}
}
// 2. 将数组尾到要插入元素下标间的元素将往后移
for (int j = len; j > insertIndex; j--) {
CommonUtils.swap(result, j - 1, j);
}
// 3. 插入元素
result[insertIndex] = insertElement;
return result;
}
插入一个元素到有序的数组最终实现
我们分析上诉代码,其中有两处可以进行优化,首先:步骤一和步骤可以合并一个循环进行完成,在一次循环中进行比较和移动元素,当条件不满足时下标即为要插入的元素下标,其次:步骤二中因为我们将插入元素保存进了数组的最后位置,因此我们没有必要每次都进行比较交换元素,当条件不满足时直接进行移动元素即可,因此最终代码实现可能如下:
private static int[] insertOneElementIntoSortedArray1(int[] sortedArray, int insertElement) {
assert sortedArray != null;
final int len = sortedArray.length;
// 申请一个长度大于1个有序数组的新数组来存放有序数组和要插入的元素
int[] result = new int[len + 1];
// 将有序数组复制到新数组中
for (int i = 0; i < len; i++) {
result[i] = sortedArray[i];
}
// 将插入的元素放置到新数组末尾
result[len] = insertElement;
// 依次将大于插入元素的元素往后移动
int i = len;
for (; i >= 0 && result[i - 1] > insertElement; i--) {
// 因为插入元素保存到数组的末尾,因此可以进行覆盖移动元素
result[i] = result[i - 1];
}
// result[i - 1] > insertElement条件不满足跳出循环时,其数组下标即为要插入的数组下标
result[i] = insertElement;
return result;
}
插入排序代码
通过插入一个元素到有序的数组的例子分析实现中,我们可以得到一个思路:对于一个有序数组,可以通过不断插入元素来达到排序的目的。但是当面对一个无序的数组,我们怎么得到有序数组呢?要插入的元素怎么来呢?首先对于一个无序数组,我们可以将数组的第一个元素作为有序数组,这样就可以将无序数组拆分:含有第一个元素的有序数组和后续所有元素组成的无序数组两部分;至于插入元素我们可以每次从无序数组中取得,这样我们就将无序数组的排序问题拆分成了将一个个元素插入到有序数组的子问题中,有了插入一个元素到有序的数组的例子代码实现,我们可以比较容易的得出如下的插入排序算法代码:
private static void insertSort(int[] array) {
assert array != null;
// 假定第一个元素已排序,将无序数组拆分成一个元素的有序数组和其余元素的无序数组
for (int i = 1; i < array.length; i++) {
// 每次从无序数组元素中选择一个插入元素,插入到有序数组中
// 有序数组的最后一个元素下标
int j = i;
// 待插入的元素
int temp = array[i];
// 将有序数组的元素从后往前依次将大于插入元素的元素往后移动
while (j > 0 && temp < array[j - 1]) {
array[j] = array[j - 1];
j--;
}
// 将待插入元素插入到有序数组中
array[j] = temp;
}
}
算法定义
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤
插入排序算法的算法步骤如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
总结
插入排序算法概括下就是:将数组的第一个元素作为有序数组,拆分无序数组为有序数组和无序数组两部分,依次从无序数组中取出一个元素,从后往前查找有序数组,找到相应的位置进行插入。
注:本篇描述的插入排序算法,指的都是简单的直接插入排序算法,至于其他的插入排序算法,如:二分插入排序,希尔排序等都是在直接插入排序算法的基础上的优化。