插入排序执行流程
插入排序很类似玩扑克牌时候的“插入”
在执行过程中,插入排序会将序列分为两部分,第一部分是已经排好序,有序的元素,剩下的是还没有排序的乱序的元素。
插入排序就是将一个个没有排好序的乱序元素插入排好序的两个元素之间。
也就是两步:
- 在执行过程中,插入排序会将序列分为2部分。头部是已经排好序的,尾部是待排序的。
- 从头开始扫描每一个元素。每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序。
也就是按照下面的方法:
Java代码实现
我们将一组数据中的第一个元素看作的已经有序的“序列”,剩下的全部看作是有待排序的元素。因此只要从第二个元素开始循环,判断,交换位置即可。
int[] array = new int[] {11,2,3,1,56,3,45,7,-100,23,2};
for (int begin = 1; begin < array.length; begin++) {
for(int end = begin - 1;end >= 0 ;end--) {
if(array[end + 1] >= array[end]) {
break;
}
if(array[end + 1] < array[end]) {
int tmp = array[end + 1];
array[end + 1] = array[end];
array[end] = tmp;
}
}
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + "_");
}
执行结果为:
-100_1_2_2_3_3_7_11_23_45_56_
时间复杂度分析
逆序对:顾名思义就是顺序相反的书堆,也就是从大到小的数对。例如数对<2,3,8,6,1>
它的数对就有<2,1> <3,1> <8,1> <8,6> <6,1>
五个。
插入排序的时间复杂度是与逆序数对成正相关的。
最坏的情况下,也就是逆序数对最大的时候,取数对
9 8 7 6 5 4 3 2 1
9的逆序对有8个,要交换8次,8的逆序对有7个,要交换7次,7的逆序对有6个,要交换6次…2的逆序对有1个,要交换1次。
因此可以看到对于数对
n n-1 n-2 n-3 ........ 2 1
排好序要交换(n - 1)+ (n - 2) + (n - 3) +… + 1 = (n - 1) * n / 2
因此最坏情况下其时间复杂度为O(n ^2).
同理可以推出最好情况其时间复杂度为O(n),比快速排序的O(logn)性能还好不少。
对于插入排序的代码优化
我们可以看到,上面的基础代码是每次判断交换元素,一轮循环会交换很多次元素,性能有点低。可以采取下次的优化思路:
① 先将待插入的元素备份
② 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置
③ 将待插入元素放到最终的合适位置
这样可以减少交换次数,直接用覆盖的方式提高程序性能。
对应的Java代码:
int[] array = new int[] {11,2,3,1,56,3,45,7,-100,23,2};
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
int num = array[cur];
while(cur > 0 && array[cur - 1] > num) {
array[cur] = array[cur - 1];
cur --;
}
array[cur] = num;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + "_");
}
}
进一步优化
采用二分搜索的方式。将每次的比较减少次数。
在插入元素v的过程中,可以先用二分搜索的方式,将v插入到相应的位置。
假设在[begin,end)
范围内搜索某个元素v,mid = (begin + end) / 2
,如果:v < array[mid]
,再去[begin,mid)
中二分查找。如果v > array[mid]
则去[mid + 1,end)
中二分查找。
找到第一个大于v的位置并返回,然后将该位置往后的各个元素后移一位,并且将v赋值到这个位置即可。
上代码:
public static int search(int[] array,int index) {
int begin = 0;
int end = index;
while(begin < end) {
int mid = (begin + end) >> 1;
if(array[index] < array[mid]) {
end = mid;
}else {
begin = mid + 1;
}
}
return begin;
}
@Test
public void testInsertSort() {
int[] array = new int[] {22,1,-3,56,3,6,35};
for(int i = 0;i < array.length;i++) {
int v = array[i];
int index = search(array, i);
int cur = i - 1;
while(cur >= index) {
array[cur + 1] = array[cur];
cur --;
}
array[cur + 1] = v;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + "_");
}
}
执行结果:
-3_1_3_6_22_35_56_
采用二分搜索的方法,可以有效减少插入排序过程中的比较次数,但是平均时间复杂度仍然是O(n ^2)。