<排序算法一>快速理解“直接插入排序“和“希尔排序“

一、直接插入排序

1.1导入

想象一下,当你过年回到家,满座的亲朋好友围坐一团,这时候就不得不有几幅扑克牌来充当消遣。懵逼的你被拉着加入了对局,很快,一沓乱序的扑克牌被分发到你的手中。

虽然有点懵,但你很快进入了状态,几乎是肌肉记忆般的,你从第二张扑克开始对之前的有序牌组进行插入:

手中的前四张牌为“9,5,Q,2”,插入起始点为牌“5”,有序牌组成员为“9”,将插入起始点“5”抽出,与有序牌组末尾的第一张进行对比,5<9,因此牌“9”接替5的位置,而“5”继续与有序牌组的下一张进行对比,若牌面值仍大于“5”则接替牌“9”移动后空出的位置,否则直接将牌“5”插入进牌“9”移动后空出的位置,但此时有序牌组已经被“遍历”完了,只能将牌“5”放在有序牌组的头部了。

第一次插入完成,前四张牌变为“5,9,Q,2”,此时插入起始点为牌“Q”,有序牌组为“5,9”。

进行第二次插入,抽出牌“Q”,与有序牌组末尾“9”对比,9<Q,不对“9”进行移动,将“Q”插入空位,即“Q”自身空出的位置。

第二次插入完成,前四张牌为“5,9,Q,2”,此时插入起始点为牌“2”,有序牌组为“5,9,Q“。

第三次插入:

…………以此类推

1.2直接插入排序算法

直接插入排序(Straight Insertion Sort)的基本操作时将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表,直至所有记录插入完为止,得到一个全新的有序数组。

直接插入排序算法是一个简单的插入排序算法,想必通过导入部分的内容同学已经有了一个不错的理解,下面我们就利用这个算法的思想,实现对数组的排序。

变量:

array:int[]数组,用于存储要排序的数组

KMP:int类型变量,用于存储应插入的记录

i :int类型变量,保存应插入的记录的数组下标,初始值为1

④ :int类型变量,保存当前正在进行对比的数组下标,初始值为i-1,即数组有序部分的末尾,应插入记录的前一个。

排序算法规则

1.将 array[ i ] (应插入记录的值)存入KMP,分别与 array[ j ] 进行对比

2.若 kmp < array[ j ] ,证明应插入记录应在 array[ j ] 值的前方,使 array[ j+1] = array[j] ,将array[ j ] 值后移,并 j--(注:kmp存储了array[i]的值因此不用担心覆盖后的数据丢失)

3.若 kmp >= array[ j ] ,证明应插入记录应在 array[ j ] 值的后方,找到了插入位置,使 array[ j+1 ] = kmp,结束本次插入,并  i++  &&  j = i-1

4.当 i > array.length-1 ,排序完成

代码

public static void insertSort( int[] array ) {

        //i为当前要插入的值的位置
        //遍历,从1位置开始对前方有序数组进行插入,当 i > array.length-1 ,排序完成
        for(int i = 1; i < array.length; i++) {

            //存储应插入的值
            int kmp = array[i];

            //j为当前进行插入判断的位置
            int j = i-1;

            //循环遍历被插的有序数组
            for( ; j >= 0; j--) {

                //若 kmp < array[ j ] ,证明应插入记录应在array[ j ] 值的前方
                if(kmp < array[j]) {
                    //将array[ j ] 值后移,为将来kmp插入腾出位置
                    array[j+1] = array[j];
                } else {
                    //若 kmp >= array[ j ] ,证明应插入记录应在 array[ j ] 值的后方,找到了插入位置,直接退出循环
                    break;
                }
            }

            //将kmp插入找到的位置
            array[j+1] = kmp;
        }

    }

1.3直接插入排序的复杂度分析

代码中,我们只定义了一个KMP用于存储记录,在只需要一个记录辅助空间的情况下,空间复杂度为O(1),关键在于它的时间复杂度。

最好情况下,被排序数组本身就是有序数组,每层循环判断kmp < array[ j ] 为 false后循环就结束了,也就是说每层循环在判断一次后就结束了,在有n个元素的数组中,就记录就移动了n-1次,即时间复杂度为O(N)

最坏情况下,被排序数组是逆序的情况,如“9,8,7,6,5,4,3,2,1,0”,该情况下有序段的每个元素和kmp都要移动一次,即对数组array[n]进行排序,需要移动:

\sum_{i=2}^{n} i=2+3+....+n=\frac{(n+2)(n-1)}{2}次。

所以,最坏情况下直接插入排序的时间复杂度为O(N^2)。

平均情况下,时间复杂度为O( \frac{n^{2}}{4} )。

结论:直接插入具有排序空间复杂度低的优势,排序数组越有序时间复杂度越低,适用于有序程度较高,成员较少的情况。

二、希尔排序

2.1导入

希尔排序是直接插入排序的改进型,但他不再适用于理牌了,因为身为普通人,我的大脑完全可以当成单核处理器,要他用希尔排序来理牌,属实有些为难,CPU烧了也不行。当然这是对普通人来说的,如果你自认是“戴夫”一级的,那当我没说。

好了,言归正传。作为优化型,希尔排序究竟做了那些优化才能在速度上更上一层楼,同时还让我的大脑当场死机呢?

答案就在直接排序的总结上,“排序数组越有序时间复杂度越低,适用于有序程度较高,成员较少的情况”。但从这句话管中窥豹便可得到两条优化路线,1是将排序数组基本有序,2是减少排序的成员。

设想一,将排序数组成员变得更少。这一点想要做到,将数组砍成几份只排一份是肯定不行的,那就很简单了,将分成的这几份分别进行直接插入排序是不是就完美达成要求了,每一份都排更少的成员,那么整体的排序效率必然会提高。

是不是感觉自己很聪明,好好好,现在来看第二个设想,将数组变得基本有序。

首先我们要知道什么是基本有序,所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的在中间。相比于{2,7,J,3,8,Q},基本有序更接近于{3,2,8,J,Q},或者{2,8,3,Q,J},又或者{2,1,3,6,4,7,5,8,9}。如果真的去计算,基本有序的时间复杂度大概率是要比两个有序数组拼在一起小的。

那问题来了,怎么样既对数组进行分组排序,同时又能保证数组是基本有序呢?答:将相聚某个“增量”的成员组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序

下面来瞻仰先辈D.L.Shell的聪明才智:>

2.2希尔排序算法

希尔排序(Shell Sort)是D.L.Shell于1959年提出的一种排序算法,在这之前排序算法的时间复杂度i本都是O(n^2),希尔排序算法是突破这个时间复杂度的第一批算法之一。

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成多个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行直接插入排序。然后,gap取gap/2(也可能是别的),重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。

以上图为例,对数组{1,5,2,81,14,2,46,10,2}进行希尔排序,增量gap初始为4,经过两次分组排序后,gap达到1,待排数组基本有序,此时直接插入排序的时间复杂度无限接近于最好情况O(n)。

实现代码如下:

public static void shellSort(int[] array) {
        //使gap初始值为length/2
        int gap = array.length;
        //gap/2=1后循环结束
        while(gap > 1) {
            //每次排序后gap取gap/2,缩小增量,直至为1
            gap = gap/2;
            //进行分组插排,分组间隔为gap
            shell(array, gap);
        }
    }

    public static void shell(int[] array, int gap) {
        //和直接插入排序一样,从组内的第二个成员开始排序
        //之所以为i++是为了使每一个成员都被遍历到
        //每次遍历排的是下标i所在组,i及之前的部分
        for(int i = gap; i < array.length; i++) {
            //kmp存储应插入的值
            int kmp = array[i];
            //因按增量gap划分组别,因此i所在组的前一成员下标应为i-gap
            //j不能越界,必须满足j>=0
            int j = i-gap;
            for( ; j >= 0; j-=gap) {
                if(kmp < array[j]) {
                    array[j+gap] = array[j];
                } else {
                    //若 kmp >= array[ j ] ,证明应插入记录应在 array[ j ] 值的后方,找到了插入位置,直接退出循环
                    break;
                }
            }
            //j的下一个是j+gap
            array[j+gap] = kmp;
        }
    }

可能图和代码看完后还有点懵,为了能够更好的理解希尔排序,接下来我会对第一次分组排序进行详细分析:

public static void shellSort(int[] array) {
    int gap = array.length;
    while(gap > 1) {
        gap = gap/2;
        shell(array, gap);
    }
}

1.程序开始执行,调用shellSort()方法,传入的参数array为{1,5,2,81,14,2,46,10,2}

2.定义变量gap=array.length,即gap=9

3.gap>1,进入while循环,执行语句“gap = gap/2;”,向下取整,此时gap=4

4.调用方法shell(),传入参数array、gap=4,即对所有距离为4的记录分在同一组内,以划分的组为单位进行直接插入排序。

public static void shell(int[] array, int gap) {
    for(int i = gap; i < array.length; i++) {
        int kmp = array[i];
        int j = i-gap;
        for( ; j >= 0; j-=gap) {
            if(kmp < array[j]) {
                array[j+gap] = array[j];
            } else {
                break;
            }
        }
        array[j+gap] = kmp;
    }
}

5.进入for循环,i=gap=4,i此时为第一组第二个成员的下标,kmp记录下array[i]的值,即kmp=14,执行下条语句j=i-gap,即i所在组的前一个成员下标,j=0

6.进入for循环,if语句判断array[ i ]是否小于array[ j ],14<1 == false,执行else,break退出循环

7.array[j+gap] = kmp,即array[4] = kmp

8.i++,i=5<length,循环继续,再i值5~7之间,分别对二组到四组的前两个成员进行了直接插入排序,当i++循环继续执行,则会对第一组前三个成员进行直接插入排序,若数组之后还有元素,则循环会对其他组别进行前三成员的排序……以此类推,完成一次分组排序

9.当i=8,j=4,完成对第一组的前三成员排序后,i++,此时i=9=length,循环条件不满足,结束循环,shell()方法执行完毕

10.回到shellSort()方法中的while循环,gap=4,循环继续,gap=gap/2=2,通过shell()方法以2为增量进行分组排序,得到基本有序的数组{1,2,2,5,2,10,14,81,46}

11.进行最后一次排序,此时gap=1,此时仅有唯一组进行直接插入排序,这一组就是整个array

2.3希尔排序的复杂度分析

希尔排序的特性总结
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:

感谢观看,如若本篇文章对您有帮助的话,恳请给博主一个免费的赞鼓励一下吧,栓Q了

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值