图解冒泡排序

187 阅读10分钟

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

一.什么是冒泡排序

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法

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

这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。

  • 时间复杂度:O(n²)

  • 算法稳定性:稳定排序算法

  • 实 质:把小(大)的元素往前(后)调

  • 思想: 交换思想

  • 原理:

    • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
    • 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
    • 针对所有的元素重复以上的步骤,除了最后一个。
    • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
  • 图解:(参考《Java数据结构和算法》书籍上的图片)

    • 未排序的一行人:

      image-20210826221105258-1629987068544

    • 从第一个人开始与第二个人进行比较,矮的站前面,高的继续和后面的人比,直到最后一人成为最高的,那么第一轮比较结束

      <u>在这里插入图片描述</u>

      <u>image-20210826222703603</u>

    • 然后第二轮继续比较,从第一个人开始依次比较,注意本轮不需要和最后一人(最高的)进行比较了,将第二高的人排在了倒数第二的位置,本轮结束。后续按照此规则继续比较,直到整个队列从小到大排序。如下:

      image-20210826222747767

二. 逻辑推演

现有一个数组,里面的数据为: [11,17,28,34,67,48,47,66],我们以此数据来分析:

第一轮比较:

原始数据排列情况如下:

image-20210827141708676

我们冒泡排序比较是从第一个元素开始进行,然后和第二个元素进行比较,由于11和17比较,11比17小,不需要交换;因此!紧接着应该由17和28进行比较,这时发现17比28小,位置继续保持不变!

image-20210827141851245

接下来继续由28和34进行比较:

image-20210827141956689

发现依然不变,继续让34和67进行比较:

image-20210827142030370

34比67小,依然不变。继续:

image-20210827142054635

当67和48比较的时候,由于67比48大!因此两者交互了!

image-20210827142117667

接下来:67和47进行比较,67比47大,继续交换:

image-20210827142152409

最后,67和66继续比较,发现67更大,那么继续完成交换:

image-20210827142224809

小结:

通过第一轮 7 次比较,最终将最大值交换到了最右边!

第二轮比较:

依然从第一个数进行比较,将第二大的数给排序到右边去:

image-20210827142627534

第三轮:

排序后的结果:

image-20210827142850766

第四轮:

image-20210827142909401

第五轮:

image-20210827142920676

第六轮:

image-20210827142930722

第七轮:

image-20210827142938508

小结:

第一轮比较7次

第二轮比较6次

第三轮比较5次

...

第七轮比较1次

三.冒泡排序第一版(入门)

代码:

public static void main(String[] args) {
        int[] arr = {11,17,28,34,67,48,47,66};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int[] array){
       //循环次数计数
        int count = 0;
        for (int i = 0; i <array.length-1 ; i++) {
            System.out.println("第"+(++count)+"次循环");
            for (int j = 0; j < array.length-i-1; j++) {
                int tmp = 0;
                if(array[j]>array[j+1]){
                    tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1]=tmp;
                }
            }
        }
    }

结果输出:

1次循环
第2次循环
第3次循环
第4次循环
第5次循环
第6次循环
第7次循环
[11, 17, 28, 34, 47, 48, 66, 67]

总结:

代码非常简单,使用双循环进行排序。外部循环控制所有的回合,内部循环实现每一轮的冒泡处理,先进行元素比较,在进行元素交换。

四.冒泡排序第二版(优化)

问题分析:

原始的冒泡排序有哪些可以优化的点呢?

回顾一下刚才图上描述的细节,仍然以[11, 17, 28, 34, 47, 48, 66, 67]数据为例,当排序算法分别执行第5轮、第6轮和第7轮时数列状态如下:

第5轮:

image-20210827142920676

第6轮:

image-20210827142930722

第7轮:

image-20210827142938508

优化思路:

很显然可以看出,经过第5轮排序后,整个数列已然是有序的了。可是排序算法仍然在继续执行第6轮、第7轮排序。

在这种情况下,如果能判断出数列已经有序,并作出标记,那么剩下的几轮排序就不必执行了,可以提前结束排序工作,下面我们来进行优化。

冒泡第二版优化代码:

public static void main(String[] args) {
        int[] arr ={11,17,28,34,67,48,47,66};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int[] array){
        //循环次数计数
        int count = 0;
        for (int i = 0; i <array.length-1 ; i++) {
            System.out.println("第"+(++count)+"次循环");
            //有序标记,每一轮的初始值都是true
            boolean isSorted = true;
            for (int j = 0; j < array.length-i-1; j++) {
                int tmp = 0;
                if(array[j]>array[j+1]){
                    tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1]=tmp;
                    //因为发生了元素交换所以当前不是有序的,标记为false
                    isSorted = false;
                }
            }
            //如果isSorted为true则说明,一轮循环都没有发现有元素交换,那当前已经是一个有序数列了,就不需要再排序了
            if(isSorted){
                break;
            }
        }
    }

结果输出:

1次循环
第2次循环
第3次循环
[11, 17, 28, 34, 47, 48, 66, 67]

总结:

与第1版代码比较,优化代码做了小小改动,利用了布尔变量isSorted作为标记。如果在当前排序中有发生元素交换就说明数列无序,如果没有发生元素交换就说明数列已经有序,然后直接跳出大循环。本数列为例,发现只循环了3次就完成了排序工作,提高了算法性能。

五.冒泡排序第三版(无敌)

问题分析:

为了明说问题,我们使用上面的冒泡优化算法来排序一个新数列:[3,4,2,1,5,6,7,8],这个数列的特点是前部分元素[3,4,2,1]是无序的,后半部分的元素[5,6,7,8] 是有序的。

第二版代码推演:

public static void main(String[] args) {
        int[] arr ={3,4,2,1,5,6,7,8};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

   public static void sort(int[] array){
        //循环次数计数
        int count = 0;
        for (int i = 0; i <array.length-1 ; i++) {
            System.out.println("外部大循环第"+(++count)+"次循环");
            //有序标记,每一轮的初始值都是true
            boolean isSorted = true;
            for (int j = 0; j < array.length-i-1; j++) {
                System.out.println("   内部小循环第"+(j+1)+"次循环");
                int tmp = 0;
                if(array[j]>array[j+1]){
                    tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1]=tmp;
                    //因为发生了元素交换所以当前不是有序的,标记为false
                    isSorted = false;
                }
            }
            //如果isSorted为true则说明,一轮循环都没有发现有元素交换,那当前已经是一个有序数列了,就不需要再排序了
            if(isSorted){
                break;
            }
        }
    }

打印结果:

外部大循环第1次循环
   内部小循环第1次循环
   内部小循环第2次循环
   内部小循环第3次循环
   内部小循环第4次循环
   内部小循环第5次循环
   内部小循环第6次循环
   内部小循环第7次循环
外部大循环第2次循环
   内部小循环第1次循环
   内部小循环第2次循环
   内部小循环第3次循环
   内部小循环第4次循环
   内部小循环第5次循环
   内部小循环第6次循环
外部大循环第3次循环
   内部小循环第1次循环
   内部小循环第2次循环
   内部小循环第3次循环
   内部小循环第4次循环
   内部小循环第5次循环
外部大循环第4次循环
   内部小循环第1次循环
   内部小循环第2次循环
   内部小循环第3次循环
   内部小循环第4次循环
[1, 2, 3, 4, 5, 6, 7, 8]

发现问题:

虽然我们使用了优化算法,减少了外部大循环的循环次数,但是对于内部小循环的次数仍然是7次、6次、5次、4次。然而内部小循环的5,6,7,8几个数字的判断比较是无意义的,右边的许多元素已经是有序的,可是每轮还是白白的比较了许多次。这正是接下来需要优化的另一个点。

优化思路:

这个问题关键在于对数列的有序区的界定,刚才代码中,第1轮排序过后结果为[2,3,1,4,5,6,7,8],发现最后5个元素已经属于有序区了。

因此后面的多次元素比较是没有意义的。

如何避免?我们可以在每一轮排序后,记录最后一次元素的交换位置,该位置即为无序数列边界,再往后就是有序区了。能区分有序和无序就可以只需要比较无序区即可。

冒泡第三版优化代码:

public static void main(String[] args) {
        int[] arr ={3,4,2,1,5,6,7,8};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }


    public static void sort(int[] array){
        //循环次数计数
        int count = 0;
        //**(新增代码)记录最后一次交换的位置
        int lastExchangeIndex = 0;
        //**(新增代码)无序边界,每一轮比较只需要比较到这里即可,首次比较要比完所有数字
        int sortBorder = array.length-1;
        for (int i = 0; i <array.length-1 ; i++) {
            System.out.println("外部大循环第"+(++count)+"次循环");
            //有序标记,每一轮的初始值都是true
            boolean isSorted = true;
            //**(新增代码)
            for (int j = 0; j < sortBorder; j++) {
                System.out.println("   内部小循环第"+(j+1)+"次循环");
                int tmp = 0;
                if(array[j]>array[j+1]){
                    tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1]=tmp;
                    //因为发生了元素交换所以当前不是有序的,标记为false
                    isSorted = false;
                    //**(新增代码)更新最近一次交换元素的位置
                    lastExchangeIndex = j;
                }
            }
            System.out.println();
            //**(新增代码)如果isSorted为true则说明,一轮循环都没有发现有元素交换,那当前已经是一个有序数列了,就不需要再排序了
            sortBorder = lastExchangeIndex;
            if(isSorted){
                break;
            }
        }
    }

输出结果:

外部大循环第1次循环
   内部小循环第1次循环
   内部小循环第2次循环
   内部小循环第3次循环
   内部小循环第4次循环
   内部小循环第5次循环
   内部小循环第6次循环
   内部小循环第7次循环

外部大循环第2次循环
   内部小循环第1次循环
   内部小循环第2次循环

外部大循环第3次循环
   内部小循环第1次循环

外部大循环第4次循环

[1, 2, 3, 4, 5, 6, 7, 8]

总结:

在本次代码中,sortBorder就是无序数列的边界。再每一轮排序过程中,处于sortBorder之后的元素就不需要在进行比较了,肯定是有序的。所以我们会看到只有第一轮循环,内部小循环循环了7次,并且找到无序边界。后面的N轮循环,内部的小循环只会循环到边界处,则大大降低了内部循环的循环次数。

六.总结

冒泡排序到这里其实还不是最优的,冒泡排序还有一个王者版本,被称为鸡尾酒排序。它是基于冒泡排序的一种升级排序法,鸡尾酒下回我们不见不散。

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀