排序(上):为什么插入排序比冒泡排序更加受欢迎?

        最经典、最常用的排序算法有:冒泡排序插入排序选择排序归并排序快速排序计算排序基数排序桶排序等。按照时间复杂度,将其划分为三类:  

        

        思考题:插入排序跟冒泡排序的时间复杂度都是O(n^{2}),在实际的软件开发当中,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?

1.如何评价分析一个“排序算法”?

1.1 排序算法的执行效率

       对于排序算法的执行效率,一般从如下几个方面来衡量:

  • 最好、最坏、平均情况时间复杂度
  • 时间复杂度的系数、常数、低阶
  • 比较次数和交换移动的次数

1.2 排序算法的内存消耗

        算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。针对排序算法的空间复杂度,我们把空间复杂度是O(1)的排序算法叫做原地排序

1.3 排序算法的稳定性

        仅仅利用执行效率和内存消耗来判断排序算法的好坏还不够,所以还有一个重要的指标:稳定性。这个概念的意思是在待排序的序列中存在值相等的元素,经过排序之后,相等的元素之间原有的先后顺序不变。

        举例说明:假如这里有个待排序序列:2,9,3,4,8,3,排序后是:2,3,3,4,8,9。这个数组里面有两个3,如果经过某种排序算法以后,两个3的前后顺序没有改变,那我们就把这种排序算法叫做稳定的排序算法;如果前后顺序发生了改变,那对应的算法就是不稳定的排序算法。

        比如说,现在要给电商交易系统中的“订单”排序。订单有两个属性:下单时间、订单金额。订单数据有10万条。排序需求是:1.按照订单金额从小到大排。2.对于值相同的订单金额,希望按时间从早到晚排。

       传统做法:

  1. 按照订单金额对订单数据从小到大排。
  2. 遍历排序之后的订单数据,对于每个订单金额相同的小区间,再按照下单时间来排序。

      这种排序思想很简单,但是实现起来会比较复杂。

      借助稳定排序算法的方法:

  1. 按照下单时间排序。
  2. 对排序以后的数据,利用稳定排序算法,按照订单金额重新排序。

     稳定排序算法可以保证金额相同的两个对象,在排序之后的前后顺序不变。这样经过两次排序以后,就可以达到最初的排序需求。

     

    2.冒泡排序

2.1 冒泡排序的原理

      冒泡排序只会操作相邻的两个数据。每次冒泡排序都会对相邻的两个元素进行比较,看是否满足大小关系,如果不满足,就交换。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。

     举个例子来展示一次冒泡排序的过程:对一组数据4,5,6,3,2,1从小到大排序。

     

        可以看到,经过一次冒泡排序,6这个元素已经存储在正确的位置上了。想要完成所有数据的排序,只需要进行6次这样的冒泡排序即可。

       

        当某次冒泡操作已经没有数据交换的时候,说明已经达到完全有序,不用再继续执行后面的操作。另外一个例子,6个元素只需要4次冒泡操作。

        

        冒泡排序的代码:

// 冒泡排序,表示数组,n表示数组的大小
public void bubbleSort(int[] a, int n) {
    if (n<= 1) return;
    for (int i = 0;i < n ;++i){
      // 提取退出冒泡排序的标志位
      boolean flag = false;
      for (int j = 0; j < n - i - 1;++j){
         if (a[j] >  a[j+1]){ // 交换
            int tmp = a[j];
            a[j] = a[j+1]
            a[j+1] = tmp;
            flag = true;//表示有数据交换
         }
     }
     if (!flag) break;//没有数据交换
    }
} 

2.2 冒泡排序的评价分析

2.2.1 冒泡排序是原地排序算法吗?

       冒泡排序只涉及相邻数据的交换,需要的临时空间是常数量级,所以空间复杂度是O(1),是一个原地排序算法。

2.2.2 冒泡排序是稳定的排序算法吗?

       在冒泡排序中,只有交换才能改变两个元素的前后顺序。为了保证排序算法的稳定性,当两个相邻数据值相等时,我们不做交换,这样就可以保证在相同大小的数据在排序前后不会改变顺序。所以冒泡排序是稳定的排序算法。

2.2.3 冒泡排序的时间复杂度是多少?

        最好情况,序列是有序的,只需要进行一次冒泡操作就结束,时间复杂度是O(n)。

        最坏情况,序列是倒序的,需要进行n次冒泡排序才结束,所以时间复杂度是O(n^{2})。

         

        通过“有序度”和“逆序度”来进行平均情况时间复杂度的分析。

       有序度是指数组中具有有序关系的元素对的个数。有序元素对用数学表达式表示就是:

//有序元素对:
a[i] <= a[j] //如果 i<j。

            

       同理,对于一个倒排序列的数组,比如6,5,4,3,2,1的有序度是0的数组;比如1,2,3,4,5,6是有序度为n*(n-1)/2,也就是15的。我们把这种完全有序的数组的有序度叫做满有序度

     逆序度跟有序度恰恰相反,得到一个公式是:逆序度 = 满有序度 - 有序度。我们排序就是一种增加有序度,减少逆序度的过程,达到满有序度时,说明排序结束。

      

        冒泡排序包含两个操作:比较和交换。每交换一次,有序度加1。不管算法怎么改进,交换的总次数是确定的,即为逆序度,也就是n*(n-1)/2 - 初始有序度

      最坏情况下,初始状态有序度是0,所以要进行n*(n-1)/2次交换;最好情况下,进行0次交换。可以取中间值n*(n-1)/4作为平均情况下的交换次数。比较次数肯定比交换次数多,而时间复杂度的上限是O(n^{2}),所以平均情况下的时间复杂度是O(n^{2})。

 

3.插入排序

       对于一个有序的数组,往里面添加一个新数据,如何保持数据有序?只需要遍历数组,找到数据应该插入的位置将其插入即可。

      

     上面是属于动态排序的过程,对于静态排序,可以借鉴这个思路,所以得到了插入排序算法。

      3.1 插入排序的原理

        将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间的元素,在已排序的区间站到合适的位置将其插入,并保证已排序的区间一直有序。重复这个过程,直到未排序区间中的元素为空,算法结束。

     举个例子:要排序的数据是:4,5,6,1,2,3,其中左侧是已排序区间,右侧是未排序区间。

     

     插入排序也是包含两种操作:元素的比较元素的移动。     

     插入排序的代码:

// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a ,int n){
    if (n<=1) return;
    for (int i = 1;i < n ;++i){
      int value = a[i]
      int j = i - 1;
      // 查找插入位置
      for(;j>=0;j--){
        if (a[j]>value){
          a[j+1] = a[j];//数据的移动
        } else{
         break;
         }
      }
      a[j+1] = value://插入数据
       
}

3.2 插入排序的评价分析

3.2.1 插入排序是原地排序算法吗? 

      插入排序不需要额外的存储空间,所以空间复杂度是O(1),是一个原地排序算法。

3.2.2 插入排序是稳定的排序算法吗?

      对于值相同的元素,可以将后出现的插入到前面出现的值的后面,这样就保持原有的前后顺序不改变,所以插入排序也是稳定的排序算法。

3.2.3 插入排序的时间复杂度是多少?

    最好情况下是O(n),最坏情况下是O(n^{2}),平均情况是O(n^{2})。

3.选择排序

        选择排序的思路跟插入排序的思路类似,也是分为已排序区间和未排序区间。但是选择排序每次会在未排序区间找到最小的元素,将其放到已排序区间的末尾。

      

        选择排序的空间复杂度是O(1),是一种原地排序算法。选择排序的最好情况、最坏情况、平均情况的时间复杂度都是:O(n^{2})。

        但是选择排序不是稳定的排序算法。

        比如5,8,5,2,9这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素是2,与5交换位置,那第一个5与第二个5的顺序就改变了,所以就是不稳定了。

4.解答开篇的问题

       是因为冒泡排序的数据交换要比插入排序的数据移动复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个。

// 冒泡排序中的数据交换操作:
if (a[j]>a[j+1]){
int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}


// 插入排序中数据的移动操作
if (a[j]>value){
   a[j+1] = a[j];
} else {
 break;
}

 

 

 

 

 

 

    

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值