插入排序之希尔排序
希尔排序是对直接插入排序的一个优化。
比如有这么一种情况:对一个无序数组进行从小到大的排序,但是数组的最后一个位置的数是最小的,我们要把它挪到第一个位置,其他位置的都要往后移动,要是这个数组非常大,那么直接插入排序的开销就非常大。------------希尔排序就可以解决这样的问题,也为其主要优点
希尔排序基本思想:
算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d。对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成。
希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式移动,使得排序的效率提高。需要注意的是,增量序列的最后一个增量值必须等于1才行。另外,由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。
增量序列的选择
Shell排序的执行时间依赖于增量序列。好的增量序列的共同特征:
① 最后一个增量必须为1;
② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了目前较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
Shell排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
稳定性: 希尔排序是不稳定的。
排序过程如下:
以数组{26, 53, 67, 48, 57, 13, 48, 32, 60, 50 }为例,步长序列为{5,2,1}
初始化关键字: [26, 53, 67, 48, 57, 13, 48, 32, 60, 50 ]
最后的排序结果:
13 26 32 48 48 50 53 57 60 67
代码实现:
第一种:主要关注increment如何取值 Math.ceil(increment/2)
public static void shellSort(int[] array){ double increment = array.length;//增量长度 int h,sentinel,k; while (true) { increment = (int)Math.ceil(increment/2);//逐渐减小增量长度 h = (int)increment;//确定增量长度 for (int i = 0; i < h; i++) { //用增量将序列分割,分别进行直接插入排序。随着增量变小为1,最后整体进行直接插入排序 for (int j = i+h; j < array.length; j+=h) { k = j - h; sentinel = array[j];//哨兵位 while (k >= 0 && sentinel < array[k]) { array[k+h] = array[k];//k+dk位始终虚位以待 k = k - h; } array[k+h] = sentinel; } } //当dk为1的时候,整体进行直接插入排序 if (dk == 1) { break; } } }
第二种:对比第一种increment如何取值 ,increment = data.length / 2
public static void shellSortSmallToBig(int[] data) { int j = 0; int temp = 0; for (int increment = data.length / 2; increment > 0; increment /= 2) { System.out.println("increment:" + increment); for (int i = increment; i < data.length; i++) { // System.out.println("i:" + i); temp = data[i]; for (j = i - increment; j >= 0; j -= increment) { if (temp < data[j]) { data[j + increment] = data[j]; } else { break; } } data[j + increment] = temp; } for (int i = 0; i < data.length; i++) System.out.print(data[i] + " "); } } public static void main(String[] args) { int[] data = new int[] { 26, 53, 67, 48, 57, 13, 48, 32, 60, 50 }; shellSortSmallToBig(data); System.out.println(Arrays.toString(data)); }
第三种:increment?
public static void shellSort(int[] array){ int h = 1;//增量长度,计算最大的h while (h <= array.length/3) { h = h * 3 + 1; } while (h > 0) { //进行间隔为h的插入排序 for (int i = h; i < array.length; i++) { if(array[i] < array[i - h]){ int k = i - h; sentinel = array[i]; while (k >= 0 && sentinel < array[k]) { array[k+h] = array[k]; k = k - h; } array[k+h] = sentinel; } }
h = (h -1) / 3 ; //计算出下一个h值,每次排序结束后,递减h的值 } }
疑问1:同样的一个待排序数组,increment取值方式略有不同,这使用哪种效率最高?
疑问2:能否改成并行的?
答:可以。因为,每次都针对不同的子数组进行排序,各个子数组之间是完全独立的。很容易实现并行程序。
并行实现:


代码中定义shellSortTask作为并行任务。一个shellSortTask的作用是根据给定的起始位置和h,对子数组进行排序,因为可完全并行化。
为控制线程数量,定义并行主函数pShellSort()在h大于或等于4时使用并行线程(第40行),否则退化为传统的插入排序。每次计算后,递减h的值。
参考资料:
3、《java 高并发程序设计》

浙公网安备 33010602011771号