基础排序详解 --冒泡--插入--选择


排序算法很多,最经典的、最常用的有:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序等。

插入排序和冒泡排序的时间复杂度都是相同,都是O(n2),但是在实际的软件开发里,我们使用最多的是插入排序而不说冒泡排序。

分析排序算法

排序算法的执行效率

1.最好、最坏、平均时间复杂度

我们在分析时间复杂度时,要给出最好情况、最坏情况、平均情况下的时间复杂度。除此之外还要说出最好情况、最坏情况对应的要排序的原始数据是什么样的。

2.时间复杂度的系数、常数、低阶

时间复杂度反应的是数据在数据规模n很大的一个增长趋势下的,所以会忽略系数、常熟、低阶。但是在实际中,排序的通常是10,100,1000这样规模小的数,所以这时候要考虑系数和常熟以及低阶。

3.比较次数和交换、移动次数

基于比较的排序算法会涉及两种操作,一种是比较大小,一种是元素交换或移动,在分析算法效率的时候要比较这两种操作。

排序算法的内存消耗

算法的内存 消耗可以通过空间复杂度来衡量,排序算法也不例外。但是针对排序算法的空间复杂度,还引入了一个”原地排序“的概念。原地排序是指空间复杂度为O(1)的排序算法,冒泡、插入、选择都是原地排序。

排序算法的稳定性

除了执行效率和内存消耗外,还有一个衡量指标是稳定性。排序算法的稳定性是指待排序的序列中如果存在值相等的元素,在经过排序后,想等元素的先后顺序不变。想等元素先后顺序不变的是稳定的排序算法,反之则为不稳定的排序算法。
排序算法的稳定性一般 体现在电商的订单排序,订单有两条属性,订单时间、订单金额。
假如我们希望按照金额从小到大排序,同时相同金额的订单希望下单时间从早到晚去排序。
解决思路:

1.先按下单时间给订单排序,使得所有的订单都按下单时间从早到晚排序

2.再按订单金额使用稳定的排序算法重新排序,因为第一次排序的时候以及时间有序了,所以第二次排序的时候相同金额的订单也是时间有序的。

冒泡排序

冒泡排序很简单,它只会两两比较邻居的元素,每次冒泡都是相对相邻的两个元素进行比较,看是否满足大小要求关系。如果不满足则让它互换。每次冒泡都会让最大的一个元素放在数组末尾。
在这里插入图片描述
代码如下:

    public static void bubblesort(int[] arr){/遍历整个数据,每遍历一次数组,就通过从0开始比较左右两个邻居元素,最大的往后移动,直至移动到数据最后。
        int n =  arr.length;  
        if (n<1) return; 
        for (int i=0; i<n; ++i){   //从下坐标0开始遍历整个数组,获得遍历次数
            Boolean flag = false; //建立标志,当发生数据交换时候,为true,没有发生数据交换为false,当没有打算数据交换的时候(已经排好序了),可以直接结束
            for (int j=0; j<n-1-i; ++j){  //开始遍历比较左右两个邻居元素,因为每一次遍历后就把最大值放在最后,每遍历一次就放一个,遍历后的最后元素是已经拍好顺序的了,不需要遍历,所以是n-1-i
                if (arr[j]>arr[j+1]){ //左右邻居元素比较 ,两两比较,大值交换最后,通过遍历把最大值仍到最后
                    int tmp = arr[j]; //用临时变量存放值,交换 
                    arr[j]  = arr[j+1];
                    arr[j+1] = tmp;
                    flag = true; //因为发生了数据交换,flag变true
                }
            }
            if (flag == false){//如果没发生数据交换了,就证明已经是有序的了,直接退出遍历
                break;
            }
        }
    }

排序分析

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

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

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

冒泡排序中只有交换才能改变元素位置,当元素相等的时候是不做交换的,所以是稳定的排序算法。

冒泡排序算法的时间复杂度是多少。

最好情况是数据已经是有序的了,只要一次冒泡排序就行,时间复杂度是O(1),最坏情况是倒序数据,需要进行n次冒泡排序时间复杂度是O(n2)。
平均复杂度用概率论分析太复杂了,涉及加权平均期望时间复杂度,所以这里采用有序度和逆序度来分析平均复杂度。

有序度和逆序度

有序度是指数据中有序关系的元素对的个数,即

如果i<j,a[i] < a[j]
在这里插入图片描述
对于一个倒叙数组,例如6、5、4、3、2、1,有序度是0,对于有序数组例如1、2、3、4、5、6有序度就是n*(n-1)/2,就是15。这种完全有序的数组有序度叫满有序度。逆有序度与满有序度相反。

所以得到一个公式:

逆序度 = 满有序度 - 有序度
例如4、5、6、3、2、1,有序对(4,5)(4,6)(5,6),有序度3,满有序度是6*5/2 =15,所以逆有序度是12.

对于一个数组进行冒泡排序,最坏情况下,初始状态有序度是0,所以要进行n*(n-1)/2次交换。
最好情况则是有序度是n*(n-1)/2,不需要交换。所以平均值是n*(n-1)/4.
即平均状态下要进行n*(n-1)/4次交换操作,比较操作是比交换操作多的,而冒泡排序时间复杂度最坏是O(n2),所以平均情况下时间复杂度是O(n2)。

插入排序

插入排序是将数组分为两个区间,一个是已排序区间,一个是未排序区间。初始已排序区间只有一个元素,即数组首元素。插入算法中核心思想是在未排序中的元素,跟已排序区间元素做一个动态的对比,然后找到适合自己的位置插入,保持已排序区间的元素是一只有序,直到未排序区间元素区间为0.

插入排序有两种操作,比较和移动。
当需要将未排序区间的a元素插入到已排序区间时,需要将a跟已排序区间的元素遍历依次进行排序,然后找到适合的插入点,找到插入点后,将插入点的元素往后移一位,腾出位置插入a。

对于不同的查找插入点,元素的比较次数是不同的,但是移动操作次数是固定的,插入排序的移动次数=逆序度。
在这里插入图片描述
插入排序代码:

 public static void insertionsort(int[] arr){
        int n = arr.length;
        if (n<1) return;  //判断如果只有一个元素,
        for (int i=1; i<n;++i){  //从1为边界开始,左边是排序好的数组,右边是未排序的数据
            int tmp= arr[i];   //获取未排序的数组中一个存储在tmp临时对象。
            int j = i-1;  //获取排序好的数组中的最后一位
            for (;j>=0;--j){  //从最后一位遍历排序好的数组
                if (arr[j]>tmp){  //判断临时对象跟排序数组中的最后一位对比,如果比排序好的数组中的值小,排序好的数组好就往后退一位,
                    arr[j+1] = arr[j];   //往后退位
                }else {break;} //找到合适位置了,不大不小,退出去
            }
            arr[j+1] = tmp; //把这个临时对象插入到合适的位置(j在上面的break前又做了一遍--用来判断break,所以这时候加回去)
        }
    }

插入排序分析

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

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

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

相同元素是插入带元素后面,所以是稳定

插入排序时间复杂度

最好情况是有序,不需要移动数据,只要从尾遍历到头比较n个数据即可,时间复杂度是O(n)
最坏情况是逆序,每次插入都要在已排序区间的第一个位置插入新数据,移动大量数据,所以最坏时间复杂度是O(n2)
平均情况是相当于在数据中插入一个数据,执行n次,平均复杂度是O(n2)

选择排序

选择排序也是分一个已排序区间,一个未排序区间,但是插入排序是遍历未排序区间从头到尾拿第一个未排序区间的元素跟已排序区间的元素对比然后插入。
而选择排序是先标记第一个最小值,然后右边为未排序区间,在未排序区间遍历元素跟最小值标记的元素比较,这样把未排序区间的元素最小的元素一个一个的排序在左边做已排序区间。
在这里插入图片描述

代码如下:

    public static void selectionsort(int[] arr){
        int n =arr.length;
        if (n<1) return;
        for (int i=0;i<n;++i){ //遍历整个数组
            int minindex = i; //设定第一个元素为最小下标
            for (int j=i; j<n; ++j){   //  i作为边界,左边是排序好的数组,右边是未排序好的数组,遍历右边
                if (arr[j]<arr[minindex]){  //如果在右边发现比最后一个值小,就把右边这个元素下边赋予mindex标记
                    minindex = j;
                }
            }
            int tmp = arr[i];  //让左边最后一个值跟右边minindex标记的元素互换。
            arr[i] = arr[minindex];
            arr[minindex] = tmp;
        }
    }

选择排序算法分析

选择排序空间复杂度是O(1),是原地排序算法。

最好情况和最坏情况都要对未排序区间遍历跟最小标记做比较,比较n个数据,做n次比较,最好最坏时间复杂度都是O(n2)

例如5,8,5,2,9,第一次找到最小元素2的时候,第一个5移动到第二个5后面,所以不是稳定的排序算法。

插入排序比冒泡排序受欢迎

插入排序和冒泡排序不管怎么优化,他们的交换次数/移动次数都是等于=原始数据的逆序度
但是从代码上看,冒泡排序的交换次数是比插入排序的移动是要复杂的,
在这里插入图片描述
在这里插入图片描述
冒泡排序在交换的时候涉及到了3个赋值操作,插入排序只涉及到了一个赋值操作,
例如一个数组的逆序度是K,用冒泡排序则是要进行K次交换,每次需要3个赋值语句,则是3k时间
插入排序每次移动只要一个赋值语句,则是K时间。所以插入排序效率是比冒泡好的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值