在本文中使用到的升序,降序,交换函数的代码以及插入排序的思想见:这篇博客
在上述博客中的插入排序中有介绍了插入排序的两个特点:
(1)当待排序序列的有序性比较高时,排序的效率比较高;
(2)当待排序序列的元素个数较少时,排序的效率比较高。
本文中介绍的希尔排序就是利用插入排序的两个特点来实现的。
希尔排序
例如,待排序序列为:10 30 20 60 50 30 20 70 90
(1)首先取步长gap为3,进行如下分组,然后在组内先进行插入排序:
第一组:10 60 20 排序后:10 20 60
第二组:30 50 70 排序后:30 50 70
第三组:20 30 90 排序后:20 30 90
将上述排序后的分组再按原下标进行合并:10 30 20 20 50 30 60 70 90
(2)再取步长gap为2,进行如下分组,然后在组内进行插入排序:
第一组:10 20 50 60 90 排序后:10 20 50 60 90
第二组:30 20 30 70 排序后:20 30 30 70
将上述排序后的分组再按原下标进行合并:10 20 20 30 50 30 60 70 90
(3)最后取步长gap为1,直接进行插入排序:10 20 20 30 30 50 60 70 90
在上述中首先分为多组,组内进行插入排序,可以使待排序的元素个数减少,然后利用插入排序既可以提高效率。然后多个组内排序之后,整个待排序序列的有序性比较高,,因此之后再对步长小的序列进行插入排序时,同时可以调高效率。
上述过程就是希尔排序的实现思路。不同的是,希尔排序对于步长的选取是一个希尔序列:n/2 n/4 n/8 ... 1。其中n为待排序序列的元素个数。
根据上述例题,希尔排序的代码实现思路为:取一个步长,根据该步长进行分组,然后在组内进行插入排序。
步长的选取是一个变化的过程,即有一组步长。一个步长对应一组分组,一个分组对应一个插入排序。
(1)首先通过一个循环来表示一组变化的步长,初始步长gap为size/2,之后每次在原基础上除2,即gap = gap/2;
(2)根据一个步长确定一组分组,各分组下标为:
第一个分组为:0 0+ gap 0+ 2gap,....
第二个分组为:1 1+gap 1+2gap,...
第三个分组为:2 2+gap 2+2gap,...
在一个分组内进行插入排序,在插入排序中有一边界值bound,所以对各分组进行插入排序时,各分组内边界值初始分别为:gap gap+1 gap+2 ... 所以可以令bound初始为gap,之后逐次加1,表示各分组内的边界初始值。
在进行插入排序时,因为同一分组内的各元素下标不连续,但不同分组的下标是连续的,所以:
(1)首先对第一个分组的第二个元素进行插入排序,
对第二个分组的第二个元素进行插入排序,
对第三个分组的第二个元素进行插入排序,...
(2)然后对第一个分组的第三个元素进行插入排序
对第二个分组的第三个元素进行插入排序
对第三个分组的第三个元素进行插入排序,...
(3)依次往后,...直到将所有元素排好序。
在插入排序中,各分组的某一个元素进行排序时,与该元素同一分组的上一个元素下标为:bound-gap。所以在取定某一个边界值bound对其进行插入时,此时,cur初始设置为bound,cur-1即为cur-gap。
1)每次将边界bound处的值bound_value与cur-gap处的值进行比较。
2)如果bound_value大于cur-gap处的值,则cur就是bound_value所放置的位置;
3)如果小于,则将cur-gap处的值移到cur处,然后cur = cur-gap,在进行1)~3)的操作。如果cur的值减至各分组的第一个元素的下标即cur小于gap时,则停止移动,此时cur就是bound_value所放置的位置。
根据上述思路,实现代码如下:
//希尔排序 void ShellSort(int arr[],uint64_t size,Compare cmp) { if(arr == NULL || size <= 1) { return; } int gap = size/2; //第一重循环,对待排序序列进行分组 for(;gap >= 1;gap = gap/2) { //第二重循环:计算边界值 int bound = gap; //第一次对第一组的第一个元素进行排序 //第二次对第二组的第一个元素进行排序 //...... //对第一组的第二个元素进行排序 //对第二组的第二个元素进行排序 //。。。 for(;bound < size;bound++) { //在组内定义cur进行插入排序 int cur = bound; int bound_value = arr[bound];//保存bound处的值 //这里注意cur的判断条件:假设步长取值为3 //则对应的三个分组第一个元素分别为:0,1,2 //对任一分组,cur减至每个分组的第一个元素下标时,停止前移 //因为分组的不同,对应的第一个元素下标也不同,但是任一分组第一个元素下标都是小于步长3的 //所以对任一分组,可以将条件设置为当cur的值小于步长3时停止前移 //也就是说cur大于等于步长3时仍可以前移,所以这里的条件为:cur>=gap for(;cur >= gap;cur = cur - gap) { if(cmp(arr[cur - gap],bound_value) == 1) { arr[cur] = arr[cur - gap]; } else { break; } } arr[cur] = bound_value; } } }
希尔排序的时间复杂度取决于步长序列。在本文中使用的是希尔序列,因而时间复杂度为:O(N^2)。空间复杂度为O(1)。稳定性为:不稳定
如果选取一个最优的序列,可以是时间复杂度降低至:O(N^1.3)