一、什么是希尔排序?
希尔排序法又称缩小增量法。
希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有元素分成gap组,即所有距离为gap的分在同一组内,并对每一组内的元素进行直接插入排序(本质是进行预排序,使数组更接近有序,目的是使最后一次直接插入排序提高效率)。然后取gap=gap/2或者gap=gap/3+1重复上述分组和排序的工作。当gap到达1时,数组是已经接近有序的,而对于直接插入排序在顺序有序或者接近顺序有序的数组内的时间复杂度为O(N),效率很高,当gap=1,即以一个元素为一组,就相当于对整个数组进行直接插入排序了。得到的就是有序数组了。也就排好序了。
希尔排序的动图演示如下:
二、如何实现希尔排序?
代码如下:
void InsertSort(int* a, int n) { int end = 0; int tmp = 0; int i = 0; for (i = 0; i < n - 1; i++) { end = i ;//end位置为i tmp = a[end + 1];//则需要插入的数的位置为end+1,即end的后一个 //tmp这个待插入的数与从end开始往前直到0的数比较,直到找到比tmp小的数就break; while (end >= 0) { if (tmp < a[end]) { a[end + 1] = a[end]; --end; } else { break; } } //来到这里无论是break跳出循环还是while循环结束跳出循环,tmp都是 // 放在end+1的位置上,while循环结束跳出的话就是把tmp插入到第一个 // 位置,break跳出是插入到中间位置或者最后一个位置 a[end + 1] = tmp; } } void ShellSort1(int* a, int n) { //首先可以把gap初始化为3,但不一定是3,可以自己选一个适当的数定义gap, //gap是几就说明距离为gap的数分到同一组内,进行分组预排序 int gap = 3; int i = 0; int end = 0; int tmp = 0; int j = 0; //以gap为距离的每一组数都要进行排序 //一定是gap组数,只是它们不一定是平均的(每组有n/gap个数,一共必定是gap组) for (j = 0; j < gap; j++) { //i+=gap刚好就是对距离为gap的一组元素进行预排序, // 这里跑一次循环就是对一组间隔为gap的数进行了 // 一次直接插入排序,那这一组数就变成了有序的了, // 预排序是为了使整个数组更接近有序。 // 其实把这里的gap初始化为1就是一次直接插入排序。 // 所以可以对照着直接插入排序来理解这里的。 //这里的i需要小于n-gap,具体说明看上图 for (i = j; i < n - gap; i += gap) { end = i; tmp = a[end + gap]; while (end >= 0) { //往前找>=tmp的数 //如果tmp比前一个数小,那就挪动数据(这里的“一”是以gap为单位的) //再往前找,直到找到或者循环结束为止 if (tmp < a[end]) { a[end + gap] = a[end]; end -= gap; } //如果找到了就break了 else { break; } } //来到这里无论是while循环结束还是中途break出来都 //是把tmp放到a[end+gap]的位置,while循环结束跳出 //证明这里是头插而已 a[end + gap] = tmp; } } //最后必须进行一次gap=1的直接插入排序才能使数组有序 //经过了前面的预排序,整个数组已经更接近于 //有序的了,所以直接插入排序的效率会很高 InsertSort(a, n); }
以上代码的两个循环也可以转化成一个循环,就是gap组的数按下标顺序进行插入排序:先对黑色组的第二个数进行插入排序(相对于黑色组内的数插入),然后不直接对黑色组的第三个数插入,而是对红色组的第二个数进行插入排序(相对于红色组内的数插入),然后再对蓝色组的第二个数进行插入排序(相对于蓝色组内的数插入),下一次才又回到黑色组的第三个数进行插入排序,以此类推,就能完成gap组数的预排序了。
代码如下:
void ShellSort2(int* a, int n) { int gap = 3; int i = 0; int end = 0; int tmp = 0; //这里变成i++,gap组数同时进行插入排序,其它都跟上面的没有变化 for (i = 0; i < n - gap; i ++ ) { end = i; tmp = a[end + gap]; //走一趟就把一个数插入到自己分组内适当的位置 while (end >= 0) { if (tmp < a[end]) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = tmp; } InsertSort(a, n); }
上面的这个写法有什么缺陷吗?答案是有的。
如果像以上这样写,就只进行一次预排序,这样显然是不能够很好地使整个数组变得接近于有序的,所以,需要进行多组预排序,才能形成一个更接近于有序的数组,最后一次的直接插入排序才能变得更加高效。
那这个gap又该如何去控制呢?
这里介绍两种方法:
第一、可以采取二分法,就是每进行了一次gap组的元素的预排序之后,使gap=gap/2重新分组,再进行预排序,一次类推,直到gap等于1(gap最后必然会等于1),也就是最后一次是进行gap为1的直接插入排序,这样的话进行多次预排序后原数组就会非常接近于有序数组,使用插入排序会接近于最好的情况,这样排序的效率就会提高很多。
第二、可以每次使gap=gap/3+1,最后gap也一定会变成1,因为到最后gap无论是1,还是2,或者是3,除以3都等于0,再+1一定等于1,这也能保证最后一次进行的是gap=1的直接插入排序,也能让数组有序。
所以下面这个经过对gap进行动态变化的,实现过多次预排序的才是真正的希尔排序。
代码如下:
void ShellSort3(int* a, int n) { int gap = n; int i = 0; int end = 0; int tmp = 0; while (gap > 1) { gap /= 2; //gap = gap / 3 + 1; for (i = 0; i < n - gap; i++) { end = i; tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end]) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = tmp; } } }
三、希尔排序的时间复杂度是多少?
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算这个时间复杂度,因此在好多书中给出的希尔排序的时间复杂度都不固定。
在不同的书本上面希尔排序的时间复杂度都各有差异,因为我的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照O(n^1.25)到O(1.6N ^1.25)次方来算。