希尔排序,要从插入排序的劣势说起,如果插入的数据需要排到有序数组最前方,那插入排序的效率会非常低下。因为插入的数据要逐个和有序数组数据进行比较。
为了解决这个问题,就有了希尔排序,其实就是在插入排序的基础上,做了一定的优化。
以数组int arr = {9, 4, 7, 8, 5, 0, 6, 2, 3, 1}为例,在做排序之前,依据下标进行分组。
第一次分为五组:{arr[0],arr[5]},{arr[1],arr[6]},{arr[2],arr[7]},{arr[3],arr[8]},{arr[4],arr[9]},然后对五个分组内数据进行排序。此时数组中相对较小的数据,基本排列在了数据前一半索引。
在第一次排序的基础上,继续进行第二次分组。
第二次分为二组:{arr[0],arr[2],arr[4],arr[6],arr[8]},{arr[1],arr[3],arr[5],arr[7],arr[9]},然后对这2个分组分别进行排序。
第三次,对所有数据进行排序,此时数组已接近有序,所以排序速度会很快。
在上面例子的基础上进行归纳,则可以得出希尔排序的步骤,便是先对数据进行分组,对各个分组进行排序,构造接近有序集合,最后对所有数据进行排序。
希尔排序使用的排序算法又分为交换排序和插入排序。
为了方便理解,第一次使用交换排序进行分组内排序,代码如下:
// static int[] list = {9, 4, 7, 8, 5, 0, 6, 2, 3, 1}; static int[] list = new int[800]; static { for (int i = 0; i < list.length; i++) { list[i] = (int) (Math.random() * 8000000); } }
long l = System.currentTimeMillis(); System.out.println(Arrays.toString(list)); //分为五组 for (int i = 0; i < list.length - 5; i++) { for (int j = i; j < list.length - 5; j += 5) { if (list[j] > list[j + 5]) { int temp = list[j]; list[j] = list[j + 5]; list[j + 5] = temp; } } } System.out.println(Arrays.toString(list)); //分为两组 for (int i = 0; i < list.length - 2; i++) { for (int j = i; j < list.length - 2; j += 2) { if (list[j] > list[j + 2]) { int temp = list[j]; list[j] = list[j + 2]; list[j + 2] = temp; } } } System.out.println(Arrays.toString(list)); //分为一组 for (int i = 0; i < list.length - 1; i++) { for (int j = i; j < list.length - 1; j += 1) { if (list[j] > list[j + 1]) { int temp = list[j]; list[j] = list[j + 1]; list[j + 1] = temp; } } } System.out.println(Arrays.toString(list)); long s = System.currentTimeMillis(); System.out.println(s - l);
归纳总结后,使用交换排序的希尔排序代码如下:
long l = System.currentTimeMillis(); //二分数组 for (int gap = list.length / 2; gap >= 1; gap /= 2) { for (int i = 0; i < list.length - gap; i++) { for (int j = i; j < list.length - gap; j += gap) { if (list[j] > list[j + gap]) { int temp = list[j]; list[j] = list[j + gap]; list[j + gap] = temp; } } } } long s = System.currentTimeMillis(); System.out.println(s - l);
经过实际测试发现,使用交换排序的希尔排序速度并没有明显提升,8w条数据,耗时6s。
那么我们在此基础上,继续改进,使用插入排序来实现:
long l = System.currentTimeMillis(); //拆分数组需要的次数 for (int gap = list.length / 2; gap >= 1; gap /= 2) { //正式拆分数组,根据步长得到每一个拆分后的数组 for (int i = 0; i < gap; i++) { //遍历拆分数组 for (int j = i + gap; j < list.length; j += gap) { //对数组进行插入排序 int insertValue = list[j]; int insertIndex = j; //如果插入值的索引大于等于数组第一位的索引,且插入值小于有序数组数据,用有序数组最末值覆盖插入索引值。 //此时连续2个索引会存在相同的有序数组最大值,数组索引向前一位,把插入值插入 while (insertIndex >= i + gap && insertValue < list[insertIndex - gap]) { list[insertIndex] = list[insertIndex - gap]; insertIndex -= gap; } list[insertIndex] = insertValue; } } } long s = System.currentTimeMillis(); System.out.println(s - l);
经过测试,8w数据耗时14ms,80w耗时148ms,800w耗时2.4s,相对于普通排序,有了明显提升。