标准实现:
/**
* 希尔排序,是一种改进的插入排序。它是基于插入排序在元素基本有序的情况下效率很高(会中断比较直接返回)这一特性。
* 核心思想:以不同的间隔来分割待排序的序列,间隔按照某种规律减小,直到1,间隔的选择一定程度上影响希尔排序的
* 效率。被分割成不同的子序列后,依次对每个子序列进行插入排序,然后用更小的间隔分割序列再进行排序。
* 直到间隔为1,也就是我们的插入排序,此时元素基本就已经有序。整体看来效率会比一般插入排序高。
* 以几为间隔就相当于把这个序列分成了几个子序列。如: 1 4 6 7 8 3 4.以2 为间隔就是 (1 6 8 4)、
* (4 7 3)这样。就是这个数以差距2跳着找亲戚,然后下个数也是以差距2跳着找,但是再下一个数的时候,你想想此时
* 就和第一个数重复了,它其实是第一个数哪一组的!!
*
* @param src 数据源。
*/
public static void standardShellSort(int[] src){
int len = src.length;
//gap是间隔,此时初始化就已数组长度的一般
int gap;
//间隔按照2倍减少直到为1.
for(gap = len/2 ;gap > 0 ;gap /= 2){
//i到gap为一组数的第一个元素的集合,其它的数就是这个数加上gap。
//所以要大于0小于gap.来标识每个组的第一个元素。
for(int i=0;i<gap;i++){
//每个组的元素,如:i = 0的时候直接就是找到了整个第一组元素,然后进行插入排序
此处是每一个分组里面的所有的元素
for(int j = i+gap;j < len ;j += gap){
//如果发现比前面的数小,此处不仅仅交换一下就行了,应该一直比较到最前面(类似冒泡)
if(src[j] < src[j-gap]){
int k = j-gap;
int temp = src[j];
//当前面还有元素,并且当前元素比前面的元素小的时候,需要往前移动。
//此时用的就是改进版的插入,不是交换,而是直接赋值
while(k >= 0 && temp < src[k]){
//不用担心覆盖前面的,因为已经拿出来了temp;
src[k+gap] = src[k];
//循环执行。
k -= gap;
}
//最后把拿出来的那个元素放在找到的位置。符合要求执行完后k就会又见一次gap,
//显然最后出来的时候应该k+gap为合适的值。
//其实理解的不深。
src[k+gap] = temp;
}
}
}
}
}
改进思想的实现:
/**
* 这是另一种拆分思路的,前面的拆分思路是以每个组的首元素为起点,然后每次加gap找到所有的元素
* 进行插入排序,执行完第一组在执行第二组。
* 而此处那?思想是循环整个数组在这个数组是++的,就是索引一个个增加。只不过按照对应的gap进行处理。
* 找到这个索引后,和它的(索引-gap)个元素一次比较,每次都是调gap的间隔,直到比较到合适的位置。
* 这样其实还是各自在处理自 己组的数,只不过是由于i++轮流处理了。你处理一次,我处理一次。而且还是
* 标准的插入,往前面比较的。
*/
public static void sort(int[] src) {
int len = src.length;
for (int gap = len/2; gap > 0; gap /= 2) {
for (int i = gap; i < len; i++) {
//此时注意j-gap >= 0.必须包含等于,很明显等于的时候j、j - gap两个也应该比较。
//不然第一个数就没办法参与排序了
for (int j = i; (j - gap) >= 0 && src[j] < src[j - gap]; j -= gap) {
int temp = src[j];
src[j] = src[j - gap];
src[j - gap] = temp;
}
//gap = 5 排序前 7 1 4 0 0 2 0 4 2 2 此时就会分成5组如下:
// 7 2
// 1 0
// 4 4
// 0 2
// 0 2
//需要(j - gap) >= 0 && src[j] < src[j - gap];两重判断
//第一组i = 5 符合,会排序。(2 7)
//第二组i = 6 符合,会排序。(0 1)
//第三组 i = 7 符合,但不符合第二个小于判断所以不会排序。(4 4)
//
此处也能证明是稳定的,但是考虑到各个组还会合并,所以最终是不稳定的。//第四组 i = 8 符合,但不符合第二个小于判断所以不会排序。(0 2)
//第五组 i = 9 符合,但不符合第二个小于判断所以不会排序。(0 2)
//排序完:2 0 4 0 0 7 1 4 2 2
//gap = 2 排序前 2 0 4 0 0 7 1 4 2 2 此时会分成 2组.
// 2 4 0 1 2
// 0 0 7 4 2
//需要进行如下两个j-gap>0,并且src[j] < src[j - gap]
//此时可以注意假如第一个判断那是>0的时候,包含第一个数的那个组永远不会让它参与排序。
//第一组 :
//i = 2、 不符合,所以不会排序。( 2 4 0 1 2);
//i = 4、 符合,进行排序。( 2 0 4 1 2)、并且它还满足继续排序(0 2 4 1 2);
//i = 6、 符合,进行排序。( 0 2 1 4 2)、继续(0 1 2 4 2);
//i = 8、 符合, 进行排序.( 0 1 2 2 4);
//第二组 :
//i = 3 不符合,由于相等不会排序。( 0 0 7 4 2);(组内稳定)
//i = 5 不符合,不会排序。( 0 0 7 4 2);
//i = 7 符合, 会排序。( 0 0 4 7 2);
//i = 9 符合, 会排序。( 0 0 4 2 7)且继续排序(0 0 2 4 7);
//排序完: 0 0 1 0 2 2 2 4 4 7 .注意上面过程时前面的索引,可以看出来他们是交替执行的,排序一下
// 第一组,
然后排序第二组,然后再排序第一组,如此往复。//gap = 1; 排序前0 0 1 0 2 2 2 4 4 7。 此时就一组了。 (插入排序就是步长为1,不是0)
//需要进行如下两个j-gap>0,并且src[j] < src[j - gap]
//i = 1、不符合,不会排序(0 0 1 0 2 2 2 4 4 7)
//i = 2、不符合,不会排序(0 0 1 0 2 2 2 4 4 7)
//i = 3、符合, 会排序(0 0 0 1 2 2 2 4 4 7)
//i = 4、不符合,不会排序(0 0 0 1 2 2 2 4 4 7)
//i = 5、不符合,不会排序(0 0 0 1 2 2 2 4 4 7)
//i = 6、不符合,不会排序(0 0 0 1 2 2 2 4 4 7)
//i = 7、不符合,不会排序(0 0 0 1 2 2 2 4 4 7)
//i = 8、不符合,不会排序(0 0 0 1 2 2 2 4 4 7)
//i = 9、不符合,不会排序 (0 0 0 1 2 2 2 4 4 7)
//排完: 0 0 0 1 2 2 2 4 4 7, 最后一组也可以看出排序需要做的动作越来越少,这就是利用插入排序的特性
//gap = 0;直接退出。由于一直是自己本身j-=gap,第一个不符合要求,没必要再进行排序。
}
}
}
这样实现代码量虽然少了,但是交换的次数变多了,也就是未优化的插入排序。接下来我们进一步对它做优化处理。
最终版:
/**
* 效率更高的希尔排序,使用的是优化后的原则排序模型
* @param src 数据源
*/
public static void betterShellSort(int[] src){
int len = src.length;
int gap;
for(gap = len/2;gap > 0;gap /= 2){
for(int i = gap;i < len;i++){
//用来存放将要进行插入排序的元素。
int temp = src[i];
int j;
//利用的是先把最初的取出来,然后其它比前面小的话,就把前面的移动到后一个位置。
for(j = i;(j-gap >= 0)&&(temp < src[j-gap]);j-=gap){
src[j] = src[j-gap];
}
//最终需要放入找到的合适的位置,两种情况:
//1、到了最前面也就是j = gap的地方,放入即可
//2、没到最前面,前面的元素都比它小了。有j-=gap,得到这个位置,然后这个位置>gap.
// 但是没有前面的大了,那么就放入就好!所以就是放入j就行了。
src[j] = temp;
}
}
}
核心思想就是把插入排序的优化方式加进去。改进后和以前相同的测试环境,平均下来真的比插入快很多。
测试数据:
10000
name: 冒泡排序1 花费了 = 0ms
name: 冒泡排序2 花费了 = 0ms
name: 冒泡排序3 花费了 = 0ms
name: 冒泡排序4 花费了 = 15ms
name: 冒泡排序5 花费了 = 0ms
平均:3 (有时候甚至全都是0)
50000
name: 冒泡排序1 花费了 = 15ms
name: 冒泡排序2 花费了 = 0ms
name: 冒泡排序3 花费了 = 0ms
name: 冒泡排序4 花费了 = 15ms
name: 冒泡排序5 花费了 = 0ms
平均 6
100000
name: 冒泡排序1 花费了 = 32ms
name: 冒泡排序2 花费了 = 15ms
name: 冒泡排序3 花费了 = 16ms
name: 冒泡排序4 花费了 = 15ms
name: 冒泡排序5 花费了 = 16ms
平均:19
1000000
name: 冒泡排序1 花费了 = 187ms
name: 冒泡排序2 花费了 = 187ms
name: 冒泡排序3 花费了 = 172ms
name: 冒泡排序4 花费了 = 171ms
name: 冒泡排序5 花费了 = 156ms
平均:174
5000000
name: 冒泡排序1 花费了 = 1047ms
name: 冒泡排序2 花费了 = 983ms
name: 冒泡排序3 花费了 = 951ms
name: 冒泡排序4 花费了 = 936ms
name: 冒泡排序5 花费了 = 952ms
平均: 972
由于比一般的排序块多了,导致前面相对较少的数字量根本看不出什么,所以额外多加了两组。
时间复杂度:
希尔排序的时间复杂度与增量序列的选取有关。希尔排序时间复杂度的下界是n*log2n,适合于中等规模的排序, 一般来
说对于希尔排序,由于结合了插入排序的特性,所以它的时间复杂度一般都远小于n^2.
稳定性:不稳定。
我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的
插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
关于增量(间隔)
1、最后的曾量一定要是1。
2、据听说用素数比较好。
3、一般我就取数组的一般,每次都折一般。