小学生图解排序算法:①冒泡排序

冒泡排序

是一种计算机科学领域的较简单的排序算法。名字由来是因为越大的元素会经典交换慢慢“浮”到数列的顶端,故名。(指从大到小的排序)——某百科的解释


算法原理

对一个给定长度为n的随机无序数组a[],以从小到大排序为例。

  1. 从第1位开始,相邻的2个数字两两互相比大小,如果前者比后者大,则双方交换数值。然后值大者再与后一位比较……通过相邻的数字不断两两比较(后者大则双方交换数值),值大者的下标将一步一步向后移动。
  2. 第一轮循环:数组所有值按步骤1中的方式比对,最终数组中的最大值移动到最末位(a[n-1],数组下标从0开始)。
  3. 第二轮循环:数组前n-1位(a[0]至a[n-2])按步骤1中的方式比对,最终前n-1位中的最大值移动到倒数第2位(a[n-2])。
  4. 第三轮循环:数组前n-2位(a[0]至a[n-3])按步骤1中的方式比对,最终前n-2位中的最大值移动到倒数第3位(a[n-3])。
  5. ……
  6. 第 x 轮循环:数组前n-x+1位(a[0]至a[n-x])按步骤1中的方式比对,最终前n-x+1位中的最大值移动到倒数第x位(a[n-x])。(x<=n-1)
  7. ……
  8. 第n-1轮循环:数组前2位(a[0]至a[1])按步骤1中的方式比对,最终前2位中的最大值移动到第2位(下标1)。此时全部排序完成。

图解过程

算法原理虽然比较简单,但仅通过文字阅读比较空洞和模糊,接下来以图解一个简单数组冒泡排序的过程来进行说明,会形象一些。建议看懂图解后再看后面的代码。

以对给定的无序数组进行从小到大排序为例。

图解说明

  • 从图解过程可以看出,我们对长度为5的数组进行了4次循环,分别用来确定末位a[4]、倒数第二位a[3]、a[2]、正数第二位a[1]位的值(思考:为什么第一位a[0]的值不用通过再一次循环来确定?)。

  • 因此我们猜想对于长度为n的数组,需要n-1次循环来从末向前依次确定每次的最大值(即依次确定a[n-1]/a[n-2]/……/a[2]/a[1]的值)。

  • 用 int类型 i 来表示第几次循环的话,则 i 取值为1至n-1间,即 1 <= i <= n-1。不过,因为数组下标从0开始,并且后面要用到a[i]的形式来表示数组元素,因此为了保持一致,我们将 i 从0开始取值,所以 i 的取值变成了 0 <= i <= n-2。当然,为了和数组末位的下标n-1保持一致,一般写成 0 <= i < n-1,其实对于int类型来说,i <=n-2 和 i < n-1 意思是一样的。(程序是多人协作的,为了方便代码的维护,建议使用约定俗成的写法。)

  • 根据以上分析,有了以下代码。

for(int i = 0; i < n - 1; i++){ //n是数组长度
}
  • 接下来,我们对第 i 次循环期间所发生的两两比对情况作个梳理。

    1. 第一轮(i=0)时,所有数字参与了比对,即元素范围为第1位a[0]至末位第5位a[4],确定了第5位(a[4])的值;
    2. 第二轮(i=1)时,前4个数字参与了比对,即元素范围为第1位a[0]至第4位a[3],确定第4位(a[3])的值;
    3. 第三轮(i=2)时,前3个数字参与了比对,即元素范围为第1位a[0]至第3位a[2],确定第3位(a[3])的值;
    4. 第四轮(i=3)时,前2个数字参与了比对,即元素范围为第1位a[0]至第2位a[1],确定第2位(a[3])的值;
  • 对数组的操作是通过元素下标来操作的,因此,我们重点关注一下 i 与下标的关系。进行两两比对的是相邻2个数,因此有2个下标相邻数组元素参与。由此我们用int类型 j 来表示前一个数字的下标,那么后一个数字的下标则是 j+1。

  • 通过观察我们发现,每次比对都是从下标0开始,则我们猜想 j 的取值为 0 <= j ;还可以发现后一个数字的下标(j+1)和第几轮(或者 i)有密切关系,其下标(j+1)在交换到最后时最大,此时为: (j+1) + i = n -1,即最大的 j = (n - 1) - i - 1, 其中 n 是数组长度。

  • 因此,在每次循环交换期间 j 的取值范围为: 0 <= j <= (n - 1) - i - 1 。因为 n - 1 为数组最末位的下标,n 为数组长度,所以依然根据约定俗成的写法,把 n - 1 换成 n ,把右侧的 <= 换成 <,即 0 <= j < n - i - 1 (取值范围和上一个式子是一样的)。

  • 综合以上分析,我们可以尝试写出冒泡算法的核心代码。

核心代码

public static void bubbleSort(int[] a){
   int i, j, temp;
   for(i = 0; i < a.length - 1; i++){
      for(j = 0; j < a.length - 1 - i; j++){
         if(a[j] > a[j+1]){
            int temp = a[j];
            a[j] = a[j+1];
            a[j+1] = temp;
         }
      }
   }
}

对于给定要排序的数组,其内部元素的次序可能是多种多样的。例如:

{9, 1, 2, 3, 4, 5, 6, 7, 8};

我们发现,只要一次大循环将9放到最末位,数组其实成了有序的。但如果按照刚才的核心代码的运行方式,在9排到末位后,依然会继续不断地循环比对下去。遇到这种情况,是否有什么方法将其中断呢?这样可以避免不必要的资源浪费。

排好9后,数组变成这样:

{1, 2, 3, 4, 5, 6, 7, 8, 9 };

此时再下一轮循环对前8个数字(a[0]~a[7])进行依次两两对比完后,发现没有任何2个前后相邻的数字会交换位置。也就意味着剩余的所有数据,都是前小后大的有序排列,排序已经完成了,没有必要再继续循环比对排序了。

因此,在代码中,我们可以尝试增加一个flag状态标识位作为开关。

优化版代码

public static void bubbleSort(int[] a){
   int i, j, temp;
   boolean flag = true;//标识位开关
   for(i = 0; i < a.length - 1; i++){
      flag = false;//在两两比对循环前,重置为false。
      for(j = 0; j < a.length - 1 - i; j++){
         if(a[j] > a[j+1]){ 
            temp = a[j];
            a[j] = a[j+1];
            a[j+1] = temp;
            flag = true;//两两比对循环时有任何一次交换,说明可能未排完
         }
      }

       //如果两两比对循环完,flag依然为false,说明没有出现前者大于后者的情况,即任意2个前后相邻的数字都是有序的,说明排序完成,中断后面的循环。
       if(!flag) break;
   }
}

当然,if ( !flag ) break; 这行代码也可以放在外层for循环的条件中去,代码更精练一些。如:

for(int i = 0; i < a.length - 1 && flag; i++){//实际项目中,flag请改成有意义的名称
}

冒泡排序是稳定的算法

含义:我们接触算法时,常常会说这种算法是稳定的,或者不稳定的。所谓算法稳定性,并非指算法在不同时候运行可能导致结果不稳定(如果真这样,那就是天大的Error了),而是指相同的数值在排序前及排序后,前后相对位置是否发生了改变。如果改变了,则说这种算法是不稳定的,反之,即是稳定的。

依然以上文的图解为例,当两个9相遇时,双方并未作交换,它们在全部排序完成后的相对位置与排序前是一致的,即是稳定的。

当然,如果你的代码写成下面这样,那么在双方相等时,也会交换值,前后顺序便会发生改变,也就变成了不稳定的算法。

if(a[j] >= a[j+1]){ //把 > 写成 >=
    int temp = a[j];
    a[j] = a[j+1];
    a[j+1] = temp;
}

一般说来,冒泡排序算法本身是稳定的算法,但蹩脚的程序员也能把它变成不稳定的【微笑脸】。


说明

本文为个人学习笔记,如有细节错误或描述歧义,请留言告知,谢谢!
本文首发于博客专栏:http://Windows9.Win/bubble_sort_algorithm/

你可能感兴趣的:(算法Algorithm,数据结构,算法,数据结构与算法分析,排序算法,冒泡排序)