描述
插入排序是一种简单的排序方法,其基本操作是将后续元素插入到已排好的有序列表中,从而得到一个新的、记录数量增1的有序表。当所有元素都被插入时,整个序列排序完成。
原理
假设有一组无序序列 R1, R2, ... , Rn。
- 将第一个元素视为元素已排序的有序序列。
- 再依次把 R2, R3, ... , Rn 插入到这个有序序列中。所以,我们需要一个外部循环,从第2个元素扫描到第n个元素。
- 接下来进行元素插入过程,假设要将 Ri插入到前面有序的序列中。由前面所述,我们可知,插入Ri时,前 i-1 个元素已经有序。所以需要将Ri 和R1 ~ Ri-1 进行比较,确定要插入的位置。一般从后往前比较,即从第 i-1个元素开始向前进行比较。
排序的过程和打扑克牌一样,按顺序将手牌一一排序。需要注意的是:为了给插入的元素腾出空间,我们需要将其余所有元素向后移动。为了更好理解,假设已经排序到Ri,对于1到i-1的元素是已经有序的了,当Ri从i-1开始逐一比较时,假设需要插入的位置是3,那么从3到i-1的元素都需要后移,为插入的元素Ri腾出空间,i-1处的元素移动到i处。
下面图中展示了插入排序的过程:灰色部分表示没有移动的元素,红色部分表示交换的元素。
性质
- 平均情况:N^2/4 次比较,N^2/4次交换;
- 最坏情况:N^2/2次比较,N^2/2次交换;
- 最好情况:N-1次比较,0次交换;
- 平均时间复杂度: O(N^2)
- 最好时间复杂度: O(N)
排序对初始数据的顺序是敏感的,有序度越高插入排序越有效。当初始数据是已排序序列时,时间复杂度为O(N)。插入排序对小数组非常有效。
插入排序
为了更好的理解插入排序,我们按照上面的思路写出代码,代码如下:
/**
* Insertion Sort
*/
public class InsertionSorted extends AbstractSorted {
public <T extends Comparable<T>> void sort(T[] arrays) {
// 开始一趟有序列扫描,当前i之前的数据视为有序序列
for (int i = 1; i < arrays.length; i ++) {
// 当前需要被插入比较的元素
T inserted = arrays[i];
// 寻找插入位置,从后向前与i之前的有序序列一一比较,如果比插入元素大,则向前移动
int j = i - 1;
while (j >= 0 && greater(arrays[j], inserted)) {
// 元素向后移动(j位置的元素移动到j+1)
arrays[j + 1] = arrays[j];
j --;
}
// 找到插入位置(如果j没有变化, 其实inserted还是插入在原来位置,所以这里加j+1!=i限制)
if (j + 1 != i) {
arrays[j + 1] = inserted;
}
String msg = String.format("第%2d次 ->: %s", i, Arrays.toString(arrays));
System.out.println(msg);
}
}
}
上面的方式能更清晰了解排序过程中的比较和交换,但是代码还不够简洁,下面提供更简洁的写法。
/**
* Insertion Sort
*/
public class InsertionSorted extends AbstractSorted {
public <T extends Comparable<T>> void sort(T[] arrays) {
// 开始一趟有序列扫描,当前i之前的数据视为有序序列
for (int i = 1; i < arrays.length; i ++) {
// 寻找位置,从后向前与i之前的有序序列一一比较,找到比当前元素小的位置
for (int j = i; j >= 0 && less(arrays[j], arrays[j - 1]; j --)) {
exch(arrays, j, j - 1);
}
String msg = String.format("第%2d次 ->: %s", i, Arrays.toString(arrays));
System.out.println(msg);
}
}
}
测试代码
/**
* 排序测试
*/
@RunWith(JUnit4.class)
public class SortedTest {
private Integer[] data;
@Before
public void setUp() {
Random random = new Random();
data = new Integer[10];
for (int i = 0; i < data.length; i ++) {
data[i] = random.nextInt(10000);
}
}
/**
* 测试插入排序
*/
@Test
public void testInsertionSorted() {
long start = System.currentTimeMillis();
Sorted sorted = new InsertionSorted();
System.out.println("排序前: " + Arrays.toString(data));
sorted.sort(data);
System.out.println("排序后: " + Arrays.toString(data));
System.out.println("执行时间: " + (System.currentTimeMillis() - start) + " ms");
}
}
测试结果
排序前: [4075, 3969, 6077, 3345, 6942, 8945, 8489, 2890, 482, 2619]
第 1次 ->: [3969, 4075, 6077, 3345, 6942, 8945, 8489, 2890, 482, 2619]
第 2次 ->: [3969, 4075, 6077, 3345, 6942, 8945, 8489, 2890, 482, 2619]
第 3次 ->: [3345, 3969, 4075, 6077, 6942, 8945, 8489, 2890, 482, 2619]
第 4次 ->: [3345, 3969, 4075, 6077, 6942, 8945, 8489, 2890, 482, 2619]
第 5次 ->: [3345, 3969, 4075, 6077, 6942, 8945, 8489, 2890, 482, 2619]
第 6次 ->: [3345, 3969, 4075, 6077, 6942, 8489, 8945, 2890, 482, 2619]
第 7次 ->: [2890, 3345, 3969, 4075, 6077, 6942, 8489, 8945, 482, 2619]
第 8次 ->: [482, 2890, 3345, 3969, 4075, 6077, 6942, 8489, 8945, 2619]
第 9次 ->: [482, 2619, 2890, 3345, 3969, 4075, 6077, 6942, 8489, 8945]
排序后: [482, 2619, 2890, 3345, 3969, 4075, 6077, 6942, 8489, 8945]
执行时间: 6 ms
注意
可能会有小伙伴会有这样的疑问,在上面的代码中明明有两层for循环,时间复杂度应该是O(N^2)才对。为什么排序对于初始数据敏感了?为什么对于已经排序好的数据时间复杂度是O(N)?程序的运行时间是以执行循环体内部而言的,对于已经排序好但数据,exch()方法从来不会执行,内循环并不执行。
参考资料
《Algorithms, 4th Edition》https://algs4.cs.princeton.edu/21elementary/