告别选择排序,咋们聊聊插入排序
这是我之前写的冒泡排序的l链接,希望你看完这两篇博客可以按照我的标题说的做。
选择排序的问题
之前你写选择的时候不是一直在喷冒泡吗?好吧,一个好的广告是拿一个老牌的产品去和新的产品比,然后吊打,比如小米和友商,干翻友商。
- 每一趟选择排序最多排好两个数字(最大值和最小值),其他数字的顺序并不会发生变化。
- 无论是多大多小的数据都会拿来比较,假设数组的长度很长,然后有一段区域的值普遍都很小,但是我们寻找最大值都是要进行一遍排序操作。
插入排序思想
我们日常打牌是不是要理一理牌,我们都是怎么做的呢,我们会将没有理的牌中拿出一张牌插入到已经理好的牌中的合适的位置,这也是插入排序的思想。
1. 算法步骤
- 拿无序序列中的首元素去与有序序列中的每一个元素比较并插入到合适的位置
- 重复上述操作直至排序完成。
2. 动图演示
3.程序设计
不知道SortAlgorithm和下面使用的测试方法的建议看一下我这篇关于测试代码的博文,不看也没事。
- 我们先搭建基本框架,insertValue为我们准备插入到有序序列的值
package com.atguigu.sortalgorithm;
/**
* 插入排序
*/
public class InsertSort implements SortAlgorithm{
/**
* 排序
*
* @param array 数组
*/
@Override
public void sort(int[] array) {
for (int i = 0; i < array.length; i++) {
//待插入的值,下标
int insertValue=array[i];
-------
}
}
}
就像我们打牌一样理牌肯定有一个标准,比如我们发的第一张牌。我们认为第一张牌组成的长度为1的序列就是我们理好的牌堆,所以第一次进循环我们应该什么事情都不做。
public void sort(int[] array) {
for (int i = 0; i < array.length; i++) {
//待插入的值,下标
int insertValue=array[i];
//第一次什么都不做
if(i==0){
continue;
}
-------
}
}
第二次循环,i=1,我们可以与i=0进行比较,如果array[1]<array[0],交换。
public void sort(int[] array) {
for (int i = 0; i < array.length; i++) {
//待插入的值,下标
int insertValue=array[i];
if(i==0){
continue;
}
if (insertValue<array[i-1]){
//交换
reverse(array,i-1,i);
}
}
}
第三次进行循环,应该先和array[1]比,如果比array[1]大,说明array[2]比有序的序列的最大值还要大,那么就什么事都不敢,下一次循环,array[2]也默认成为了有序的序列当中的一员。
如果举一个极端的例子,如果说array[2]比array[1]和array[0]都要小,我们是不是可以先交换array[2]和array[1],然后array[2]就跑到之前array[1]的位置成为新的array[1],然后再交换array[1]和array[0]。
不知道这么说你们理不理解,我这边贴一张图帮助理解
比有序序列的最后一个数大,直接拼接
比有序序列的最小的数还要小,依次替换
我们来更新一下代码
public void sort(int[] array) {
for (int i = 0; i < array.length; i++) {
for(int j=i;j>0;j--){
if(i==0){
continue;
}
if (array[j-1]>array[j]){
//交换
reverse(array,j-1,j);
}
else{
break;
}
}
}
}
首先待插入的数字的下标是i,我们的j的初始化值等于i,我们比较一下j-1和j,如果我们要插入的数(下标为i也是现在的j)小于j-1(目前j-1是有序序列最大的数字),那么我们交换一下,如上图所示,交换完毕之后还要比较一下j-2和j-1,直到 j =0时,j = 1 时是最后一次进入循环,是0和1比,不会越界。
我们发现这个过程有点像冒泡排序,所不同的是有序列表已经排好了,所以无需双循环,而且如果要插入的数字找到了它的合数位置,我们用break来跳出循环了,因为再前面的数据肯定比它要小,不用再比了
我们发现如果i=0,那么内侧的for循环是进不去的,因为j=i=0>0不成立,所以我们可以吧continue省略
public void sort(int[] array) {
for (int i = 0; i < array.length; i++) {
for(int j=i;j>0;j--){
if (array[j-1]>array[j]){
//交换
reverse(array,j-1,j);
}
else{
break;
}
}
}
}
我们的代码看看还有问题,首先外侧循环的i是要插入的值的下标,内循环的j是有序的序列的最大值的下标,初始值为0。每次我们都执行交换操作直至找到它的位置,我们跑一下代码测试一下
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
//可行性测试
SortUtils.simpleTest(InsertSort.class);
//性能测试
SortUtils.timeComplexityTest(InsertSort.class);
}
测试结果
===================InsertSort=====================
原数组[93, -173, 93, -27, 219, 3, 228, -445, -340, -342]
排序后数组[-445, -342, -340, -173, -27, 3, 93, 93, 219, 228]
排序结果正确!
开始计算....
InsertSort:1912ms
这是上一个博文选择排序的测试结果
开始计算....
SelectSortPro:1951ms
说好干翻友商呢?就这?
嗯…其实我有点标题党的味道,但是我们的InsetSort是普通版本的,而SelectSort是Pro版本的,比较当然是拿一个系列的比了,接下来我们请出我们的插入排序Pro
代码优化
其实没什么好优化的,就是把交换操作改为覆盖操作
步骤:
- 我们先把要插入的数用一个变量insertValue存储下来
- 比较一下我们要插入的值和有序序列的最大值,如果insertValue则把有序序列的最大值覆盖我们的要插入的值所对应下标的位置,然后我们继续向左比较
- 重复操作2,直至找到我们要插入的位置,用insertValue值去覆盖我们要插入的位置的值。
不好理解吧,没事看图!!!
下一次比较
继续下一次比较,我们发现2比4小,我们插入的位置应该在2之后,如图,由于连续的覆盖操作,6相当于移动到了之前4的位置,5移动到6的位置,而5原来的位置没有人去覆盖,所以有两个五,所以我们的临时变量应该覆盖到首次找到的比我们要插入的数字(也就是图中的4)要小的数字(图中的2)的位置的下标+1的位置。
这里我们直接粘Pro的代码,相信你们可以理解
package com.atguigu.sortalgorithm;
/**
* 插入排序
*/
public class InsertSortPro implements SortAlgorithm{
/**
* 排序
*
* @param array 数组
*/
@Override
public void sort(int[] array) {
for (int i = 0; i < array.length; i++) {
//待插入的值,下标
int insertValue=array[i];
int index=i;
while (index>=1 && insertValue<array[index-1]){
array[index]=array[index-1];
index--;
}
array[index]=insertValue;
}
}
}
测试结果
===================InsertSortPro=====================
原数组[-118, -312, 76, -465, 407, 14, 119, -167, -151, -112]
排序后数组[-465, -312, -167, -151, -118, -112, 14, 76, 119, 407]
排序结果正确!
开始计算....
InsertSortPro:587ms
总结
时间复杂度
在平均情况下,插入排序仅减少了关键字间的比较次数,而记录的移动次数不变。因此折半插入排序的时间复杂度仍为O(n2)。
空间复杂度
折半插入排序所需附加存储空间和直接排序相同,只需要一个记录的辅助空间r[0],所以空间复杂度为O(1)。
算法特点
1.稳定排序
2.因为要进行折半查找,所以只能用于顺序结构,不能用于链式结构。
2适合初始记录无序,n较大时的情况。