2021新年算法小专题—3.大话排序算法上(Java)

今天来点轻松的!其实看了很多很难的算法,最后很容易把最基本的东西给忽略了,所以新年的第三个小算法专题我们来复习一下常见的排序算法,主要会涉及插入排序、冒泡排序、快速排序、归并排序等等,写一份标准的代码。

本篇文章介绍四个简单排序算法—选择、插入、希尔、冒泡排序。下一篇文章将介绍更多高级排序算法。
文章正在陆续更新中,感谢你的关注~

初级排序—选择排序

概述

每次都遍历一下数组,找到一个最小的数拿出来,与第一个位置的元素进行交换,然后继续在剩余的元素中找第二小的数,与第二个位置的元素进行交换…像这样每次都选择剩余未排序元素中最小者的排序方式就是选择排序

实现起来比较简单,每次进行遍历,找到最小的元素所在的下标,与当前循环进行的次数对应的下标做值的交换即可。

参考代码
int []a=new int[]{2,1,7,8,10,9};// example for sort
        
for(int i=0;i<a.length;i++){
            
    int min=i;// 记录本轮最小值的下标
    for(int j=i+1;j<a.length;j++){// 更新最小值下标
        if(a[j]<=a[i]){
            min=j;
        }
    }
    int tmp=a[i];// 把最小值和当前循环对应下标元素做交换
    a[i]=a[min];
    a[min]=tmp;
}

可以有一个小改进,比如当我们在第二轮交换前,1,2,7,8,10,9中,我们发现第二小的数2已经排在2这个位置上了,因此就不用交换了,我们可以在交换出加入条件if(min!=i),满足这个条件才交换。

初级排序—插入排序

概述

插入排序十分适合部分有序的数组排序,它通过构建有序序列,对于未排序数据,在已排序序列中进行遍历,找到合适位置并插入。具体实现可以是:把整个数组分成两个部分:有序部分和无序部分,有序部分初始时有一个值(数组的第一个元素),其他元素都是无序部分,无序元素最后要插入到有序部分中

image-20210220173857479

循环中将从无序部分(第二个元素)开始,每次取出一个元素i,与当前有序部分中的元素进行挨个比较,有序部分的元素指针j从下标0开始,找到第一个大于i的位置停下,这个位置就是i要插入的位置。如果有序部分中的元素都比i小则无需插入。如上面图中的例子,7与有序部分中的元素2进行比较,比2大,所以不用插入,直接排在有序部分的末尾即可。

image-20210220174213996

继续以图为例,现在要插入1了,有序部分的第一个元素2就比1大,是第一个大于i的位置,因此1将被插入到这个位置。确定了插入位置就要开始移动元素为插入腾空间了,需要将ji-1下标对应的元素都右移一个位置,这样才能将i放入j位置。

image-20210220174651538

本例中需要把2、7右移,这样才能腾出j=0这个位置,但是这样做i所在的位置的值1会被覆盖,所以我们在移动之前使用遍历tmp把这个值保存起来。移动后再把这个值写到j=0位置上,结果如下图所示。

image-20210220174913618

然后在下一轮循环中,我们的i会增加1,即有序部分增加1,无序部分减少1,如下图所示。

image-20210220175121226

按同样的方式,把6也与有序部分中的元素进行挨个比较,找到第一个比i大的元素的位置,插入。结果如下图。

image-20210220180317917

然后把8和有序部分的元素依次比较,发现都比8小,因此8不需要额外移动。最终i走到数组末尾,数组排序完成。这就是一种插入排序的过程。

参考代码
       int []a={2,7,1,6,8,3,4};// test data
       int tmp=0;// 记录待插入元素 插入时用
        
       for(int i=1;i<a.length;i++){
           for(int j=0;j<i;j++){
               if(a[i]<a[j]){// 找到第一个比a[i]大的位置停下
                   tmp=a[i];// 备份待插入元素
                   for(int k=i-1;k>=j;k--)// 右移
                       a[k+1]=a[k];
                   a[j]=tmp;// 插入
               }
           }
       }

拓展:也可以在遍历有序序列时从后往前遍历,即从i-1开始到0找到第一个比tmp小或者相等的元素就停下,这个位置j就是要插入的位置,然后还是一样进行右移。在下面的希尔排序的代码中我们使用的就是这种倒序的遍历有序序列的方式。

初级排序—希尔排序

先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。

概述

前面介绍的排序算法都是和相邻的元素进行比较交换,这样适合那种基本有序的数组排序,而对于数组基本无序的情况,相邻元素的交换过于缓慢,需要经过很多次比较才能交换到两个相隔较远的元素,希尔排序正是改进了这一点。它通过交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

希尔排序中有一个增量(步长)的概念,因为希尔排序是通过把数组分成若干个不相邻的子序列分别进行排序的,有一个增量hh刚开始很大,比如为5,代表每隔5个元素为一组子序列,如下面的数(第一行),在以5为增量后形成了5个子序列(5个颜色),这五个子序列将分别进行插入排序,即组内(相同颜色)的数字可以进行交换至有序。

image-20210220212122840

我们把相同颜色的子序列内部进行插入排序,然后就得到了一个局部有序的数组,这是我们令间隔h=5的结果。下一步我们令h=2,则代表每隔两个元素就为同一个自序列,最终分成两个子序列(蓝色和红色),然后对两种颜色的数字分别插入排序,得到下图最下一行的结果。

image-20210220213112196

这时整个数组已经基本有序了,我们使用一次插入排序,也就是令间隔h=1,就能很快地完成这个数组的排序了。最终结果如下图:

image-20210220213657618

这个过程就是希尔排序了,它改进了插入排序只能交换相邻元素、对完全无序的数组不友好的缺点,通过设置**间隔h**来让间隔较远的元素直接“见面”,逐步缩小h1,使原来完全无序的数组变成局部有序的数组,最终使用一次全局的插入排序就能完成排序。

下面说一下间隔h的选取:有多种h序列可以选择,更好的间隔序列能带来时间复杂度上的优化。一般我们使用1,4,13,40,121,364······即**h=3h+1h1开始取)就能有很好的性能了,最坏的情况下需要比较N3/2次,不会达到N2**。当然也可以使用其他序列,本文就不展开了。

上面图片中的例子中,有两个相等的元素5,我将其中一个(位于前面的)5做了下划线的标记,可最终排序的结果里它跑到了另一个5的后面,这说明希尔排序是不稳定的排序

参考代码
    int []a={9,1,2,5,7,4,8,3,5};// test data

    int h=1;
    int len=a.length;
    while(h<len/3){// 确定h的最大值,在3h+1序列中找
        h=h*3+1;
    }
    int i,j;
    while (h>=1){
        for(i=h;i<len;i++){// 子序列 局部插入排序
            int tmp=a[i];
            // 倒序遍历有序序列,指针j,当遍历到第一个元素小于等于tmp了就停下,插入到j这个位置。
            for(j=i-h;j>=0 && a[j]>tmp ;j-=h)//
                a[j+h]=a[j];
            a[j+h]=tmp;
        }
        h=h/3;// 每轮结束都缩小间隔h,按h序列缩小
    }

初级排序—冒泡排序

概述

是一种我们经常提及的简单排序算法。它属于一种交换排序,即通过对数组中的数据不断进行交换来完成排序。两个数比较大小,通过交换使较大的数“下沉”到数组尾部,较小的数“冒起来”:到数组头部。具体实现步骤如下:

  1. 用一个指针i指向第一个元素,然后比较和它相邻的元素i+1。如果前一个比后一个大(逆序)就交换他们两个。
  2. 指针i 加一,对这个元素和它相邻的元素重复第一步。直到i走到数组倒数第二个元素(最后一对)。完成这一轮交换后,最大的元素就下沉到数组末尾了。
  3. 指针i继续加一,重复上述步骤。每次都会成功的将一个较大的数安放到末尾(下图中橙色的元素)。

这个过程通过两层循环控制:

  • 第一个循环(外循环),负责把需要冒泡的那个数字排除在外;
  • 第二个循环(内循环),负责两两比较交换。

下面的动图形象展示了排序过程。

img
参考代码
    public void bubbleSort(int []r){
        for(int i=0;i<r.length-1;i++){// len-1个数冒泡,需要循环len-1次

            for(int j=0;j<r.length-1;j++){// j与j+1两两比较,需要循环len-1次
                if(r[j]>r[j+1]){// 逆序则进行交换
                    int temp=r[j];
                    r[j]=r[j+1];
                    r[j+1]=temp;
                }
            }
        }
    }
改进

我们都知道冒泡算法是有改进空间的。以上面的代码为例,如果一个数组本身就是有序的,或者经过几轮循环已经有序了,上面的代码还是会继续进行比较,直到循环结束。显然我们在发现数组已经有序的时候停下,更节省时间。具体做法是增加一个flag变量,初始为false,当一次内循环中没有发生任何两两交换的时候,说明数组是有序的,无需任何交换。我们在发生交换时将flag置为true,然后循环外判断如果flagfalse,说明没有发生过交换,直接break退出。如下面的代码:

    //  1.加入判断,当一轮交换中没有发现任何逆序数字,说明数组已经有序,退出循环。
    public void bubbleSort1(int []r){

        for(int i=0;i<r.length-1;i++){
            boolean flag=false;
            for(int j=0;j<r.length-1;j++){

                if(r[j]>r[j+1]){// 逆序则交换
                    flag=true;
                    int temp=r[j];
                    r[j]=r[j+1];
                    r[j+1]=temp;
                }
            }
            if(!flag){
                break;
            }
        }
    }

循环的时候也有改进的空间。我们外循环每进行一次,都会在数组末尾固定好一个有序的数字,如上图中的黄色数字,因此我们在后续的遍历时就无需遍历到数组末尾,而是遍历到这些黄色数字之前就可以了,对内部循环条件做如下修改:

//  2.每次交换都会让最大的数字跑到后面,所以每次循环都可以减少一次交换的判断。
    public void bubbleSort2(int []r){

        for(int i=0;i<r.length-1;i++){
            boolean flag=false;
            for(int j=0;j<r.length-1-i;j++){

                if(r[j]>r[j+1]){// 逆序则交换
                    flag=true;
                    int temp=r[j];
                    r[j]=r[j+1];
                    r[j+1]=temp;
                }
            }
            if(!flag){
                break;
            }
        }
    }

参考

  1. 算法(第四版)—人民邮电出版社
  2. 希尔排序—知乎(链接
  3. 冒泡排序—今日头条(链接

更多文章:

  1. 新年算法小专题1.滑动窗口(Java)

  2. 新年算法小专题1.滑动窗口刷题(Java)

  3. 新年算法小专题2.股票买卖(Java)

  4. 新年算法小专题2.股票买卖刷题(Java)

你的喜欢是我创作的动力,喜欢请关注,感谢每一个喜欢~
如有问题欢迎进行交流~
水平所限,如有错误请海涵,欢迎指正~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值