您的位置:首页 > 编程语言 > Java开发

算法入门---java语言实现的希尔排序小结

2017-04-08 11:53 316 查看
标准实现:

/**

*  希尔排序,是一种改进的插入排序。它是基于插入排序在元素基本有序的情况下效率很高(会中断比较直接返回)这一特性。

*  核心思想:以不同的间隔来分割待排序的序列,间隔按照某种规律减小,直到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、一般我就取数组的一般,每次都折一般。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息