插入排序 以及其改进型 希尔排序的Java实现以及实际性能对比
简单对插入排序和希尔排序做一个介绍,以及给出Java实现,之后简单分析下两者的时间复杂度,以及实际表现
插入排序:
这里用一张图简单表示一下:
希尔排序:
带有一种步长的排序,步长的最后一步总是1
可以认为普通的插入排序是希尔排序的最后一步,但因为希尔排序在最后一步前已调整好了大部分元素的顺序,因此相对耗时的交换赋值操作比插入排序要少很多。
不光是在最后一步,从总体来看希尔排序比插入排序快就快在交换赋值操作要少。
在稍后的测试中可以看到,在数据量超过10000的时候,插入排序的效率远远不如希尔排序
希尔排序的具体原理及实现参考:
插入排序和希尔排序的Java实现
插入排序:
package com.ryo.algorithm.sort; /** * 插入排序 * @author shiin */ public class InsertionSort implements Sort{ @Override public int[] sort(int[] arr) { int temp ; int j; for(int i=1 ;i<arr.length ;i++) { temp = arr[i]; j = i-1; //这里要写成强且 否则当j=-1时第二个判断将会抛出outofindex异常 while(j > -1 && arr[j] > temp) { arr[j+1] = arr[j]; j--; } arr[j+1] = temp; } return arr; } }
希尔排序:
package com.ryo.algorithm.sort; /** * 这里默认数组的长度将大于37 * @author shiin * */ public class ShellSort implements Sort{ //步长序列 private int[] steplist = {373,181,83,37,17,7,3,1}; @Override public int[] sort(int[] arr) { //输入数组长度 int len = arr.length; //进行子排序次数 int times = 0; //当前步长 int step; //临时存储的变量 int temp; //临时存储的索引 int m; for(;times < steplist.length ;times++) { step = steplist[times]; //这里i相当于是一个偏移量 最大偏移不能超过步长 for(int i=0 ;i<step ;i++) { //j,j+step,j+2*step......为被分到一组的下标 //这里开始和直接插入排序相似,可以认为直接插入排序的step为1 for(int j=i ;j<len &(j+step)<len;j=j+step) { temp = arr[j+step]; m = j; while(m > -1 && arr[m] > temp) { arr[m+step] = arr[m]; m = m-step; } arr[m+step] = temp; } } } return arr; } }
用于测试的类:
packagecom.ryo.algorithm.sort; import com.ryo.algorithm.util.Util; public class DoSort { private int[] result = new int[20000]; public static void main(String[] args) { DoSort ds = new DoSort(); long before = System.currentTimeMillis(); //在这一行更换需要测试的方法 ds.testShellSort(); long after = System.currentTimeMillis(); for(intn : ds.getResult()) { System.out.println(n); } System.out.println("total time : "+(after-before)+"ms"); } public DoSort() { for(inti=0 ;i<result.length ;i++) { //这里是已写好的随机创建0-10000随机数的方法 result[i] = Util.random(); } } public void testInsertionSort() { new InsertionSort().sort(result); } public void testShellSort() { new ShellSort().sort(result); } public int[] getResult() { return this.result; } }
性能对比:
测试环境:Jdk 1.8.0_161 eclipse Version: Oxygen.3a Release (4.7.3a)
测试数据:数组长度为20000,存有0-20000随机整数, 事先跑数次使得运行时间稳定,再取三次运行的平均值 ,初始化等额外操作均不在计时范围内
运行时间结果:
插入排序
平均时间:147ms
希尔排序
平均时间:7ms
实际上因为这里取的步长最大才为373,多取几个步长运行时间还会更小,步长的取值直接影响到希尔排序的效率,且影响非常大
这里在private int[] steplist ={373,181,83,37,17,7,3,1};的基础上附加一个733和1607,再跑起来发现时间基本可以稳定在6ms
由这里的结果对比可以看到,在数据量超过10000的时候,希尔排序的效率远远大于直接插入排序,若数据量继续增大,插入排序所需要的时间将增长的更快。
关于直接插入排序和希尔排序的时间复杂度
直接插入排序的平均时间复杂度为O(n^2),最好情况下(即已排好序)时间复杂度为O(n),最差情况下(即逆序排列)的时间复杂度为O(n^2)
希尔排序的平均时间复杂度在选择较好的步长时应该能接近O(n^1.3),最坏和最优的时间复杂度和直接插入排序相同
由于希尔排序的效率跟程序选择的步长紧密相关,关于希尔排序步长选择的研究其实是一个数学难题
在本程序中对比实际效率:
理论效率倍数:20000^2 / 20000^1.3 = 1025
实际效率倍数:147 / 7 = 21
1025 >> 21
由于1025只是理论效率倍数,但由于时间复杂度的计算中只包括了主要的耗时操作,一些非关键操作直接认为耗时为0,且还有Java环境、内存存取等影响,造成了数值偏差较大
但我们依然可以看到两种算法实际效率之间存在较大的差距