java实现排序(3)-希尔排序

引言

希尔排序也是经典的排序算法之一,其实本质上还是插入排序,不过它对插入排序做了进一步的优化。在本篇博文中会详细介绍希尔排序,讨论算法性能,用代码实现希尔排序并解释为什么它相对于插入排序有了进一步的优化。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

希尔排序

希尔排序是Donald Shell发明的,所以用他的名字命名。在希尔排序中,通过比较相距一定间隔的元素来工作,这一定间隔我们称之为增量。在一趟趟的排序中,不断的减小增量,直到比较相邻元素(增量为1)的最后一趟排序为止。所以希尔排序也叫缩减增量排序

为了后面更好的理解,我这里先用个例子来描述希尔排序的过程:

这里写图片描述
我们来一步步分析一下上述过程:
目标数据为:8,2,3,7,5
增量序列为:2,1

A、在增量为2的时候,其实数据的分组情况是:
①组:arr[0],arr[2],arr[4],即 8,3,5为一组;
②组:arr[1],arr[3],即2,7为一组。

那么我们就需要分别对两组实现在上一篇博文介绍的简单插入排序来做排序处理:
处理第①组,第一组第一个默认为是已排好序的序列(单独一个数字,当然可以算已排好序的),拿出3来进行插入,发现3<8且8前面已经没有数据了,所以就把3插入8前面,也就是我们上面说的“2增量1次”;接着处理5,发现5<8,但是5>3。所以就把5插入到3和8之间,注意这个插入和冒泡的区别,在插入排序中并不是两两交换顺序,而是要寻找待插入的数字的准确位置,插入到3和8之间的过程是,把8往后移动一位,空出5的正确插入位置!!这就是上面说的“2增量3次
处理第②组,第二组第一个默认为是已排好序的序列, 发现7比2大,直接把7插入到2的后面。也就是上面描述的“2增量2次”。
这里有的小伙伴会问,为什么总结的执行顺序和上面讨论的不一致呢?不一致的原因是,我们并不是一个小组一个小组处理的,比如说上面的原始数据中,数组①和数组②是穿插在一起处理的,所以变成了处理①组再处理②组的情况。

B、在增量为1的时候,其实数据就是只有一组了:
①组 3,2,5,7,8。注意,这里的数据是在增量为2**处理之后**的数据。
其实增量为1的时候,就是对所有的数据进行一次简单插入排序。且只有增量为1才能保证数据是排序完毕的,所以在设置增量的时候最后一个增量肯定是1,不然你的希尔排序就是不正确的。这里的排序过程就是简单的插入排序,会进行N-1轮(这里为4轮)的简单插入排序,可以查看前面介绍插入排序的博文,这里不再赘述。

通过上面的简单了解,我们在进一步介绍一下希尔排序的核心模块:增量序列。
从上面的例子可以看出,只要保证最后一个增量为1,那么任何增量序列都是可行的。对于上面的排序既可以[2、1]作为增量,你还可以用 [3、1];[4、1];[4、3、1]等等。所以,这里就会暴露出一个问题,如何选择增量将会直接影响到你排序的性能问题,这个在后面我会用代码测试运行时间来阐述。
希尔排序的时间复杂度依赖于增量序列的选择,所以不同的增量序列,他们所导致的时间复杂度是不同的。

希尔增量序列

希尔增量序列是指这样的一种增量序列:h = N/2,h/2…..1。的过程,就如上面的有5个元素进行排序,那么它的希尔增量序列就是5/2 = 2, 2/2 = 1,也就是[2、1]增量序列。同时在希尔增量序列下,希尔排序的时间复杂度为O(N^2)

Hibbard增量序列

Hibbard增量序列是一个更有经验的序列,它描述这么一个序列:1,3,7…….2^k-1。关键的因素是相邻的增量是不存在公因子的。这个时候的希尔排序时间复杂度就为O(N^(3/2))

在后面,我们尝试比较①希尔增量下的希尔排序和②Hibbard增量下的希尔排序的运行效率。
在代码实现之前,我们总结以下实现的规律:
①存在一个增量序列,且这个增量序列的最后一位为1。同时在每一个增量序列下需要处理一次数据,那么狠明显,我们需要依赖增量序列进行一个for循环操作。
②对于按增量序列分组后的数据,对每个组的数据其实是一次简单的插入排序。所以核心底层应该还是简单插入排序。
③我们前面也说道,虽然说每一个增量分组需要单独的进行简单的插入排序,但是作为一个整体的目标数据,进行多数组拆分显然是不可能的,所以我们需要交叉处理各个数组。

代码实现希尔增量下的希尔排序

package com.brickworkers;

import java.awt.color.ICC_ColorSpace;

/**
 * 
 * @author Brickworker
 * Date:2017年4月27日下午2:54:00 
 * 关于类ShellSort.java的描述:希尔排序
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class ShellSort {

    //希尔增量下的希尔排序
    //为了使代码更加的通用,我们用泛型来书写,同时和前几篇一样实现comparable接口
    public static<T extends Comparable<? super T>> void shellsort(T[] target){//用T表示泛型
        int j;//标记当前数据应该要插入的位置

        //第一个for循环,循环增量
        //希尔增量增量大于0,且第一个增量为目标数据总长度的一半取整,后面的增量都为前面增量的一半取整
        for(int increment = target.length / 2; increment > 0; increment /=2){

            //简单的插入排序
            //遍历目标数据,从第一个增量开始,第0个增量默认排序好的
            for(int i = increment; i < target.length; i++){
                //把带插入的数据用temp暂存起来
                T temp = target[i];
                //寻找准确的插入位置
                //其实核心是在增量的分组下寻找这个值在某一个分组下应该属于它的位置
                for(j = i; j >= increment&&temp.compareTo(target[j - increment]) < 0; j-=increment){//注意,因为是处理分组的,所以不能和简单插入排序一样j--
                    //如果当前位置的数据比前一个数据小,那么就需要把前面数据往后移动一位
                    target[j] = target[j - increment];//这里并不是冒泡的交换位置!!
                }
                target[j] = temp;//把数据插入到准确的地方,这个才是插入排序
                //打印每次排序后的结果
                String result = "";
                for (T t : target) {
                    result += t+" ";
                }
                System.out.println(increment+"增量的排序结果:" + result);

            }
        }
    }

public static void main(String[] args) {

        Integer[] target = {8,2,3,7,5};
        shellsort(target);
    }
}

    //输出结果:
//  2增量排序结果:3 2 8 7 5 
//  2增量排序结果:3 2 8 7 5 
//  2增量排序结果:3 2 5 7 8 
//  1增量排序结果:2 3 5 7 8 
//  1增量排序结果:2 3 5 7 8 
//  1增量排序结果:2 3 5 7 8 
//  1增量排序结果:2 3 5 7 8 

大家仔细查看输出结果,和我们在文章开头所描述的希尔排序示例是一模一样的。接下来我们尝试用代码实现Hibbard来实现希尔排序,有了上面的基础,这里就会简单很多,只需要把increment做一下修改即可。

package com.brickworkers;

import java.util.Random;

/**
 * 
 * @author Brickworker
 * Date:2017年4月27日下午2:54:00 
 * 关于类ShellSort.java的描述:希尔排序
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class ShellSort {

    //直接定义一种增量枚举类型
    private static enum Type{
        SHELL, HIBBARD;
    }

    public static<T extends Comparable<? super T>> void shellsort(T[] target, Type type){//修改原来的方法,新增一个增量类型参数
        //但是要保证两者的增量数是一致的,所以用size标记增量个数
        int size = 0;
        for(int increment = target.length / 2; increment > 0; increment /=2){
            size++;
        }
        switch (type) {
        case SHELL:
            long startTimeShell = System.currentTimeMillis();
            for(int increment = target.length / 2; increment > 0; increment /=2){
                dataHanding(target, increment);
            }
            System.out.println("希尔增量排序耗时:"+ (System.currentTimeMillis() - startTimeShell));
            break;
        case HIBBARD:
            long startTimeHibbard = System.currentTimeMillis();
            for(int increment = (int) (Math.pow(2, size)-1); size > 0; increment = (int) (Math.pow(2, --size)-1)){
                dataHanding(target, increment);
            }
            System.out.println("Hibbard增量排序耗时:"+ (System.currentTimeMillis() - startTimeHibbard));
            break;

        default:
            break;
        }


    }

    private static <T extends Comparable<? super T>> void dataHanding(T[] target, int increment) {
        int j ;//标记当前数据应该要插入的位置
        //简单的插入排序
        //遍历目标数据,从第一个增量开始,第0个增量默认排序好的
        for(int i = increment; i < target.length; i++){
            //把带插入的数据用temp暂存起来
            T temp = target[i];
            //寻找准确的插入位置
            //其实核心是在增量的分组下寻找这个值在某一个分组下应该属于它的位置
            for(j = i; j >= increment&&temp.compareTo(target[j - increment]) < 0; j-=increment){//注意,因为是处理分组的,所以不能和简单插入排序一样j--
                //如果当前位置的数据比前一个数据小,那么就需要把前面数据往后移动一位
                target[j] = target[j - increment];//这里并不是冒泡的交换位置!!
            }
            target[j] = temp;//把数据插入到准确的地方,这个才是插入排序
/*          //打印每次排序后的结果
            String result = "";
            for (T t : target) {
                result += t+" ";
            }
            System.out.println(increment+"增量的排序结果:" + result);*/
        }
    }


    public static void main(String[] args) {

        //制造一个2000个数据的无序序列
        Integer[] target = new Integer[20000];
        for (int i = 0; i < 20000; i++) {
            target[i] = new Random().nextInt(2000);
        }

        shellsort(target, Type.SHELL);
        shellsort(target, Type.HIBBARD);
    }
}


//输出结果:
//希尔增量排序耗时:13
//Hibbard增量排序耗时:8

用一个静态内部枚举类表示情况分类,同时用size保证两种增量的数量是一致的。因为中间的一段代码是复用的,所以我用eclipse中的Extract Method方法把中间重复部分抽离出来形成了一个新的方法。对于一个20000个数据的排序,发现用Hibbard增量的希尔排序会比希尔增量的希尔排序快的多,大家可以自己尝试。

尾记

希尔排序是插入排序的一种优化,核心仍旧是插入排序。那么为什么会使得性能有所提升呢?在简单的插入排序中,我们讨论过如果目标数据本身就是比较有序的那么它的排序效率就会高很多,因为它减少了复制的次数。那么在希尔排序中,存在优越的增量情况下,可以减少数据比较和复制的次数。所以希尔排序的高效性和增量序列有极大的关系,一个好的增量序列才能提升关键效率。如果大家有兴趣,可以把简单插入排序也列入比较目标,3中类型进行比较很快就能发现各自之间的效率问题。

希望对大家有所帮助

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章