十大排序算法之插入排序

本文首发于个人博客

前言

本系列排序包括十大经典排序算法。

  • 使用的语言为:Java
  • 结构为:
    定义抽象类Sort里面实现了,交换,大小比较等方法。例如交换两个值,直接传入下标就可以了。其他的具体排序的类都继承抽象类Sort。这样我们就能专注于算法本身。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* 返回值等于0,代表 array[i1] == array[i2]
* 返回值小于0,代表 array[i1] < array[i2]
* 返回值大于0,代表 array[i1] > array[i2]
*/
protected int cmp(int i1, int i2) {
return array[i1].compareTo(array[i2]);
}

protected int cmp(T v1, T v2) {
return v1.compareTo(v2);
}

protected void swap(int i1, int i2) {
T tmp = array[i1];
array[i1] = array[i2];
array[i2] = tmp;
}

什么是插入排序

  • 插入排序(Insertion sort)是一种简单直观且稳定的排序算法。如果有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。。

插入排序的基本思想

每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

算法稳定性

  • 插入排序是一种稳定排序算法。

是否是原地算法

  • 何为原地算法?
    • 不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入
    • 空间复杂度为 𝑂(1) 的都可以认为是原地算法
  • 非原地算法,称为 Not-in-place 或者 Out-of-place
  • 插入排序属于 In-place

时空复杂度

  • 最好时间复杂度:O(n)
  • 最坏、平均时间复杂度:O(n^2)
  • 空间复杂度:O(1)

代码

思路:第一种方案是每次比较。如果后面的比前面的小。就交换位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package YZ.Sort;

public class InsertionSort1<T extends Comparable<T>> extends Sort<T> {

@Override
protected void sort() {
// TODO Auto-generated method stub
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
while (cur>0 && cmp(array[cur],array[cur-1])<0) {
swap(cur, cur-1);
cur--;
}
}
}
}

优化

思路:将 交换 改为 挪动

  • 先将待插入的元素备份
  • 头部有序数据中比待插入元素大的,都朝尾部方向挪动一个位置
  • 将待插入元素放在最终的合适位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class InsertionSort2<T extends Comparable<T>> extends Sort<T> {

@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
T res =array[cur];
while (cur>0 && cmp(res,array[cur-1])<0) {
array[cur] = array[cur-1];
cur--;
}
array[cur] = res;
}

}

}

二分法优化

前面的代码中,想要插入一个元素,是通过逐个比较来寻找要插入的位置。其实可以通过二分法来更快速的查找位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class InsertionSort3 <T extends Comparable<T>> extends Sort<T>  {

@Override
protected void sort() {
// TODO Auto-generated method stub
for (int begin = 1; begin < array.length; begin++) {
insert(begin, searchIndex(begin));
}
}

/**
* 将source位置的元素插入到dest位置
* @param source
* @param dest
*/
private void insert(int source,int dest) {
T v = array[source];
for (int i = source; i > dest; i--) {
array[i] = array[i-1];
}
array[dest] = v;
}

/**
* 利用二分搜索找到 index 位置元素的待插入位置
* 已经排好序数组的区间范围是 [0, index)
* @param index
* @return
*/
private int searchIndex(int index) {
int begin = 0;
int end = index;
while (begin<end) {
int mid = (begin+end)>>1;
if (cmp(array[index], array[mid])<0) {
end = mid;
}else {
begin = mid +1;
}

}
return begin;
}

}

结果

数据源:从1到20000之间随机生成10000个数据来测试

Integer[] array = Integers.random(10000, 1, 20000);

结果如下:

【BubbleSort】
稳定性:true 耗时:0.481s(481ms) 比较次数:4999.50万 交换次数:2467.42万

【BubbleSort1】
稳定性:true 耗时:0.428s(428ms) 比较次数:4998.82万 交换次数:2467.42万

【BubbleSort2】
稳定性:true 耗时:0.405s(405ms) 比较次数:4993.60万 交换次数:2467.42万

【InsertionSort1】
稳定性:true 耗时:0.239s(239ms) 比较次数:2468.42万 交换次数:2467.42万

【InsertionSort2】
稳定性:true 耗时:0.186s(186ms) 比较次数:2468.42万 交换次数:0

【InsertionSort3】
稳定性:true 耗时:0.114s(114ms) 比较次数:11.90万 交换次数:0

【HeapSort】
稳定性:false 耗时:0.005s(5ms) 比较次数:23.53万 交换次数:9999

可以看到插入排序的性能高于冒泡排序,但是低于堆排序

逆序对

什么是逆序对?

数组[2,4,1]中的逆序对为<2,1> 和 <4,1>

插入排序的时间复杂度和逆序对的数量成正比关系

逆序对越多,插入排序的时间复杂度越高

当逆序对的数量极少时候,插入排序的效率特别高

甚至有时候可以比O(nlogn)级别的快速排序还要快

代码地址:

------ 本文结束感谢您的阅读 ------