【JS冒泡、选择、插入排序算法】

前言

相信排序对于每一个程序员来说都不会陌生,本节,我们一起来探讨一下三个经典排序算法:冒泡、选择和插入排序。

思考

我们都知道,在分析一个算法的好坏的时候,我们第一反应就是分析它们的时间复杂度,好的算法时间复杂度自然会低,此外,空间复杂度也是衡量它们好坏的标准,好的算法的确也会在空间复杂度上做的比较好。
诚如上述,时间复杂度、空间复杂度基本是衡量算法的标准,但是对于排序算法来说,我们还需要考虑一个因素,那就是排序算法的稳定性。

  • 排序算法的稳定性是指,在排序过程中,值相同的元素间的相对位置跟排序前的相对位置是一样的。举个例子,排序前一个数组为{3, 2, 1, 2’, 4},我们用2’来区分第二个2和第一个2,假如是稳定的排序算法,它的结果一定是这样{1, 2, 2’, 3, 4},而如果不稳定的算法,它的结果有可能是这样{1, 2’, 2, 3, 4}。
  • 为什么我们要强调稳定性呢?举个例子,假如我们需要排序一个订单,需要按照时间和价格进行升序排序,首先会先将所有订单按时间升序排序,然后再进行价格的升序排序,假如价格排序不是一个稳定的排序,那么订单的时间就有可能不会按升序排列,所以在特定情况下,排序算法的稳定性是一个比较重要的考虑因素。

一、冒泡排序

算法原理:指针重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,故名。

☀️举个例子,假如我们需要对数组{5,4,3,1,8,2}进行升序排序,那么第一轮冒泡后,8就冒泡到了最后一个位置,因为它是最大值。
在这里插入图片描述
如此重复6轮,如下图所示,即可将数组的每一个元素冒泡到自己的位置,从而完成排序。
在这里插入图片描述
js代码段如下:

    var arr=[5,4,3,1,8,2]
        for(var i=0;i<arr.length;i++){//外循环,排序的轮数
             for(var j=0;j<arr.length-1;j++){//内循环,实现每趟排序,
             //j<arr.length-1,是为了防止发生越界行为
                if(arr[j]>arr[j+1]){
                    var temp=arr[j]
                    arr[j]=arr[j+1]
                    arr[j+1]=temp
                } 
             }
        }
        console.log('排序后的数组',arr)

优化后的JS代码段如下:

 var arr=[5,4,3,1,8,2]
        for(var i=0;i<arr.length;i++){//外循环,
             for(var j=i+1;j<arr.length;j++){//内循环,j<arr.length,是为了防止发生越界行为
                if(arr[i]>arr[j]){
                    var temp=arr[i]
                    arr[i]=arr[j]
                    arr[j]=temp
                } 
             }
        }
        console.log('排序后的数组',arr)

1.时间复杂度

当数组是有序时,那么它只要遍历它一次即可,所以最好时间复杂度是O(n);如果数据是逆序时,这时就是最坏时间复杂度O(n^2),那么平均时间复杂度怎么算呢?

计算平均时间复杂度会比较麻烦,因为涉及到概率论定量分析,所以可以用以下方法来计算,以上面排序数组为例子,先来理解一下下面的概念:

  • 有序度:有序度是指一个数组有序数对的个数,例如上面数组为排序前有序数对为(4,5),(4,8),(3,5),(3,8),(1,5),(1,2),(1,8),(5,8),(2,8),所以有序度是9。
  • 满有序度:满有序度是指一个数组处于有序状态时的有序数对,还是上面数组排序完成后的有序数对有15个,用等差数列求和,得出求满有序度的通用公式为n(n-1)/2。
  • 逆序度:逆序度正好和有序度相反,所以上面数组对应的值是15-9=6,即逆序度=满有序度-有序度。

理解了这三个概念后,我们就可以知道,其实将数组排序就是有序度增加,逆序度减小的过程,所以我们排序的过程其实也是求逆序度大小的过程。

那么我们就可以计算冒泡排序的平均时间复杂度了,最好情况下,逆序度是0,最坏情况下,逆序度是n(n-1)/2,那么平均时间复杂度就取中间值,即n(n-1)/4,所以简化掉系数、常数和低阶后,平均时间复杂度为O(n^2)。

2.空间复杂度

由于冒泡排序只需要常量级的临时空间,所以空间复杂度为O(1),是一个原地排序算法。

3.稳定性

在冒泡排序中,只有当两个元素不满足条件的时候才会需要交换,所以只有后一个元素大于前一个元素时才进行交换,这时的冒泡排序是一个稳定的排序算法。

二、选择排序

算法原理:选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

☀️跟冒泡排序一样,我们分析选择排序时也是对数组{5,4,3,1,8,2}进行排序,整个排序过程如下图所示。在这里插入图片描述
经过6次循环,完成了数组排序,具体实现代码如下:

    <script>
    var arr=[5,4,3,1,8,2]
     for(var i=0; i<arr.length-1;i++){
         var minIndex=i//mindex保存的是当前无序序列中最小数的下标
         for(var j=i+1;j<arr.length;j++){
             //内循环,找当前无序序列的最小数,保存其下标
             if(arr[j]<arr[minIndex]){
             minIndex=j
            }
         }
     if(minIndex!=i){
         //当前无序序列的最小数和无序序列的第一个数进行交换
         var temp=arr[minIndex]
         arr[minIndex]=arr[i]
         arr[i]=temp
     }
    }
     console.log('排序后的数组:',arr)
    </script>

通过上面的排序过程图和代码对选择排序进行分析:
1.时间复杂度
选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都是O(n2)。因为原数组无论是否有序,进行排序时都是需要每一个元素对进行比较,当比较完成后还要进行一次交换,所以每一种情况的时间复杂度都是O(n2)。
2.空间复杂度
因为只需要用到临时空间,所以是一个原地排序算法,空间复杂度为O(1)。
3.稳定性
如下图所示,我们排序{5,5’,1},得知排序后两个5的位置已经交换,所以不是稳定排序。
在这里插入图片描述

三、插入排序

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

☀️举一个例子,同上两个排序算法一样,我们使用插入排序对数组{5,4,3,1,8,2}进行排序,流程如下图所示:
在这里插入图片描述
如上述流程可以知道,每一次从无序数组中抽第一个元素,缓存起来,然后在从已排序的最后一个元素开始比较,当该元素大于已排序元素,已排序元素后移一位,直到遇到比他小的元素,然后在比他小的元素的后一个位置插入缓存起来的元素,这样已排序数组增加一个元素,未排序数组减少一个元素,直至结束。

综上所述,实现的代码段如下:

    <script>
        var a=[543182]
        for(var i=1;i<a.length;i++){//外循环,控制程序趟数
            for(var j=i;j>0;j--){//内循环;完成每趟排序
                if(a[j-1]>a[j]){
                    temp=a[j-1]
                    a[j-1]=a[j]
                    a[j]=temp
                }
            }
        }
        console.log('排序后的数组:',a)
    </script>

通过上面的排序过程图和代码对插入排序进行分析。
1.时间复杂度
最好时间复杂度为当数组为有序时,为O(n);最坏时间复杂度为当数组逆序时,为O(n2)。已知,往一个有序数组插入一个元素的平均时间复杂度为O(n),那么进行了n次操作,所以平均时间复杂度为O(n2)。
2.空间复杂度
插入排序为原地排序,所以空间复杂度为O(1)。
3.稳定性
插入排序每一次插入的位置都会大于或者等于前一个元素,所以为稳定排序。

三种算法比较

经过上述对三个时间复杂度均为O(n^2)的算法进行分析,可以知道它们的差异如下表所示:

算法最好时间复杂度最坏时间复杂度平均时间复杂度原地排序稳定性
冒泡O(n)O(n^2)O(n^2)
选择O(n^2)O(n^2)O(n^2)×
插入O(n)O(n^2)O(n^2)

通过对比可以看到,冒泡和插入排序都是优于选择排序的,但是插入排序往往会比冒泡排序更容易被选择使用,这是为什么呢?

虽然说冒泡和插入在时间复杂度、空间复杂度、稳定上表现都是一样的,但是我们别忽略了一个事情,我们求出来的时间复杂度是忽略了系数、常数和低阶参数的,回看代码我们知道,冒泡和插入在进行交换元素的时候分别是如下这样的,假如一次赋值操作耗时为K,那么冒泡排序执行一次交换元素就需要3K时间,而插入排序只需要K。

// 冒泡排序交换元素
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;

// 插入排序交换元素
arr[j+1] = arr[j];

这只是理论分析,插入排序由于每次比较的耗时比较短,所以整体来说耗时也会比冒泡要少。

四、总结

虽然这三种排序的复杂度较高,达到O(n^2),冒泡和选择排序也不可能使用到应用实践中去,都只能停留在理论研究上,而插入排序还是以其原地排序和稳定性能够在某些特定的场景中使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值