升序
插入排序类似于打牌时码牌行为的的排序
平均时间复杂度是 O(N²)
核心思想
-
将序列分成两部分,头部是已排序好的,尾部是待排序的。
-
从头开始扫描每个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然有序。
代码实现
public class InsertionSort<E extends Comparable<E>> extends Sort<E> {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
while (cur > 0 && cmp(cur, cur - 1) < 0) {
swap(cur, --cur);
}
}
}
}
逆序对
逆序对个数和插入排序的时间复杂度是成正比的。
如果元素全逆行,那么它的时间复杂度可以达到O(n*n)
如果没有逆序对,即数组完全升序,时间复杂度是O(n)
测试输入:
【HeapSort】
稳定性:false 耗时:0.002s(2ms) 比较:1.68万 交换:999
------------------------------------------------------------------
【InsertionSort】
稳定性:true 耗时:0.006s(6ms) 比较:24.39万 交换:24.29万
------------------------------------------------------------------
【SelectionSort】
稳定性:false 耗时:0.007s(7ms) 比较:49.95万 交换:999
------------------------------------------------------------------
【BubbleSort】
稳定性:true 耗时:0.008s(8ms) 比较:21.81万 交换:13.84万
------------------------------------------------------------------
Process finished with exit code 0
优化一
可以看出,上面的写法性能很差,主要是交换次数太多了。
解决思路:将 交换 改成 挪动:
- 将待插入的元素备份。
- 将头部元素中比待插入元素大的都往后 挪动一位。
- 将备份元素插入合适的位置。
挪动比 交换代码量少多了,交换需要三步,挪动只需要一步。
private void sort2() {
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
E element = array[cur];
while (cur > 0 && cmp(element, array[cur - 1]) < 0) {
array[cur] = array[--cur];
}
array[cur] = element;
}
}
优化后的输出:
【InsertionSort2】 使用挪位代替交换
稳定性:true 耗时:0.006s(6ms) 比较:25.43万 交换:0
------------------------------------------------------------------
【InsertionSort1】最原始的
稳定性:true 耗时:0.009s(9ms) 比较:25.43万 交换:25.33万
------------------------------------------------------------------
Process finished with exit code 0
可以看到是有提升的,交换次数没了。
while循环执行的越多,优化的性能越多。其实就是 逆序对越多,优化越明显。
优化二
可以通过优化 比较次数来达到优化目的。
插入排序第二部:从头开始扫描每个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然有序。
之前找到合适的位置都是通过for循环遍历一遍,时间复杂度是O(n)。我们可以通过优化这查找位置的方法来提升时间。
注意到 **头部序列其实是排序好的,所以我们可以通过二分搜索来查找合适的位置。**二分查找的时间复杂度是O(logN)。
/**
* 找到index元素待插入的位置
* <p>
* 已经排好的区域是[0,index)
*
* @param index 待插入元素的位置
* @return 应该插入的位置
*/
private int search(int index) {
E v = array[index];
int begin = 0;
int end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (cmp(v, array[mid]) < 0) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
注意上面的代码和二分搜索有点不一样。搜索插入的位置,应该是最近大于待插入元素的位置。
知道应该插入的位置之后,我们还需要将从该位置到尾部的数据都往后移动一位,腾出位置插入目标值。
private void insert(int begin, int insertIndex) {
//备份
E element = array[begin];
//将[insertIndex,begin)位置像后挪
for (int i = begin; i > insertIndex; i--) {
array[i] = array[i - 1];
}
array[insertIndex] = element;
}
整体代码是:
private void sort3() {
for (int begin = 1; begin < array.length; begin++) {
//头部是有序的,使用二分查找,复杂度就是O(logn)
int insertIndex = search(begin);
insert(begin, insertIndex);
}
}
private void insert(int begin, int insertIndex) {
//备份
E element = array[begin];
//将[insertIndex,begin)位置像后挪
for (int i = begin; i > insertIndex; i--) {
array[i] = array[i - 1];
}
array[insertIndex] = element;
}
/**
* 找到index元素待插入的位置
* <p>
* 已经排好的区域是[0,index)
*
* @param index 待插入元素的位置
* @return 应该插入的位置
*/
private int search(int index) {
E v = array[index];
int begin = 0;
int end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (cmp(v, array[mid]) < 0) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
对比下三种方式的输出:
【InsertionSort】 二分搜索优化的
稳定性:true 耗时:0.003s(3ms) 比较:8559 交换:0
------------------------------------------------------------------
【InsertionSort2】 使用挪位代替交换
稳定性:true 耗时:0.006s(6ms) 比较:25.43万 交换:0
------------------------------------------------------------------
【InsertionSort1】最原始的
稳定性:true 耗时:0.009s(9ms) 比较:25.43万 交换:25.33万
------------------------------------------------------------------
Process finished with exit code 0
使用了二分搜索后,只是减少了查找插入位置的比较次数,但是挪动位置的时间复杂度仍然是O(N),所以插入排序的平均时间复杂度仍然是O(n²)