冒泡排序及其优化

什么是冒泡排序?

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。

这个是百度百科上比较官方的说法。用我们的白话来说就是,从头开始,依次比较两个相邻的元素,如果前一个元素大于后一个元素则交换位置(从小到大排序),直到这组数的最后一个元素。下面我们画图具体描述一下。

 

假设有如下一组数字,我们要从小到大排序

 

 

第一轮比较:

首先1和5比较,1比5小,位置不变

接着第一轮,第二次比较,5和6比较,位置依旧不变

第一轮,第三次比较,6和2比较,6比2大,所以6和2交换位置

第一轮,第四次比较,6和3比较,交换位置

第一轮,第五次比较,6和4比较,交换位置

这样,第一轮经过5次比较后,这一组数中最大的数6,就到了末尾位置。

现在这一组数的排序位置如下图,图中蓝色的,就是已经排好序的元素6。

接着我们在继续第二轮,就是重复之前的动作,只是不用再一直比到6了,只需比到4就行了,因为6已经排好位置了。

这里我用文字描述一下,就不再画图了,偷个懒。

第二轮比较:第一次比较,1和5,不交换位置:1,5,2,3,4,6

                    第二次比较,5和2,交换位置后:1,2,5,3,4,6

                    第三次比较,5和3,交换位置后:1,2,3,5,4,6

                    第四次比较,5和4,交换位置后:1,2,3,4,5,6

经过第二轮后,现在的顺序应该如下图

然后再第三轮、第四轮,一直到第五轮没有元素可以比较了。说明已经排好序了。

我们发现:每一个轮就会产生一个有序元素(蓝色部分)。又因为每一轮都会产生一个有序元素,那么待排序元素就少了一个(白色部分)。所以每一轮比较的次数就比前一轮少一次。那么我们可以总结出如下公式:对N个元素进行冒泡排序,需要比较N-1轮,每第K轮,需要比较N-K次。

我们用代码实现如下:

int[] array = {1,6,5,2,3,4};
for (int i = 0;i < array.length-1; i ++){
    for (int k = 0;k < array.length-i-1;k++){
        int temp = array[k];
        if (array[k] > array[k+1]){
            array[k] = array[k+1];
            array[k+1] = temp;
        }
    }
}

上面这个代码不难看出,其时间复杂度为O(n²),空间复杂度为O(1)。

 

优化初体验

这个效率也太低了,那么我们可以优化它吗?当然可以。其实不难发现,我们在进行第2轮后,这一组数其实已经有序了。然而之前的排序还要继续执行后面3轮,这不是傻吊吗?白白浪费时间。

怎么判断已经有序了呢?我们之前的排序是不是依次比较两个数,如果有错序就交换位置,那么如果这一轮没有发生交换位置,不就代表在这一轮,整组数已经有序了嘛?因此我们的代码可以做如下优化

int[] array = {1,6,5,2,3,4};
for (int i = 0;i < array.length-1; i ++){
    boolean flag = true;//默认这一轮没有发生交换
    for (int k = 0;k < array.length-i-1;k++){
        int temp = array[k];
        if (array[k] > array[k+1]){
            array[k] = array[k+1];
            array[k+1] = temp;
            flag = false; //发生了交换
        }
    }
    if (flag){
        break;
    }
}

这个时间复杂度虽然还是O(n²),但是在一组数中,如果大部分元素本身是有序的,就可以少执行很多不必要的循环。

 

What?还能再优化?

回顾之前的排序,每经过一轮比较,就至少能产生一个有序元素。我们姑且把这块区域称为有序区吧。如下面这个第一轮排序后,蓝色的有序区就只有一个6

我为什么要用至少呢?因为假设是这样的一组数:1,5,3,2,4,6。第一轮排序:1和5,不换;5和2,交换。5和3,交换。5和4,交换,5和6,不换。结果如下图

我们可以看到,按照之前的方法,就是只产生了一个有序元数6。但我们不难发现,其实在5和6比的时候,并没有交换位置。所以实际的情况,有序区应该是这样的

所以,我们可以发现,其实在每轮最后一次发生交换的位置K,这个位置K后面的元素应该都是有序的。这样,我们下一轮比较,就不需要再比较这个位置后面的元素了。

拿上面的例子来说,按我们之前的规则,第一轮产生了有序数6后,第二轮应该从1一直比较到5,但是有了这个规律,我们只需要比较到第一轮最后一次发生交换的位置,即数4。下面我们来看一下代码的实现

int[] array = {1,5,3,2,4,6};
int changeIndex = 0; //最后一次交换的位置
int sortIndex = 5; //有序区的边界位置

for (int i = 0;i < array.length-1;i++){
    boolean flag = true; //默认这一轮没有发生交换
    for (int k = 0;k < sortIndex;k++){
        int temp = array[k];
        if (array[k] > array[k+1]){
            array[k] = array[k+1];
            array[k+1] = temp;
            changeIndex = k; //记录最后一次发生交换的位置
            flag = false; //发生了交换
        }
    }
    sortIndex = changeIndex;
    if (flag){
        break;
    }
}

 

终极优化

你想想,我们现在的排序,是不是每一轮都要从头开始,即正向排序

那我们为什么不在每一次轮比较到最后一个元素后,在反过来比较一遍呢?

这就好比,我在A点、B点各放了一堆面值100的人民币,然后你需要拿够1000,每次只能拿100。而且一个来回每个点只能拿一次。然后你就头铁,老子就只从A点拿,再从B点回到A点再拿。你需要10个来回(实际只需9个来回,最后一次出发点A拿了就不跑了。但为了规则,拿钱就得干事还是得跑一下)。但是如果你聪明点,A点出发的时候拿一次,到了B点再拿一次,就只需要5个来回。这不就节省了一半时间。

可能这个比喻不太恰当,但为什么能提高效率,我们心里应该已经有数了。其实这个排序还有一个优雅的名字,叫做“鸡尾酒排序”。下面我们来看一下代码的实现

int[] array = {1,3,2,4,6,5};
for (int i = 0;i < array.length/2; i ++){
    boolean flag = true;//默认这一轮没有发生交换
    //从左到右,正向排序
    for (int k = 0;k < array.length-i-1;k++){
        int temp = array[k];
        if (array[k] > array[k+1]){
            array[k] = array[k+1];
            array[k+1] = temp;
            flag = false; //发生了交换
        }
    }
    if (flag){
        break;
    }          
    flag = true; //在进行反向排序前,默认没有发生交换
    for (int k = array.length-i-1;k > i;k--){
        int temp = array[k];
        if (array[k-1] > array[k]){
            array[k] = array[k-1];
            array[k-1] = temp;
            flag = false; //发生了交换
        }
    }
    if (flag){
        break;
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值