如何优雅的实现冒泡排序

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。


突然的一个想法,实现一个"优雅系列",覆盖的范围就是我们常见的几个算法,昨天算是一个开篇,已经实现了二分法查找,今天来看一看冒泡排序,后面还会有选择排序,插入排序,快速排序,希望这个系列可以让大家对常见的算法有一个简单而又清晰的认识。不要怂,就是干。

话不多说,让我们一起看看冒泡排序吧。

前提

给定一个非空数组 a,要求把 a 变成一个升序排列的数组。

算法描述

  1. 依次比较数组中相邻的两个元素,若 a[j] > a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
  2. 重复以上步骤,直到整个数组有序

算法的描述挺简单的哈,但是实现起来确有很多坑,我们先来看看最基础的实现,然后才想着优化。

算法实现基础版

上面提到一轮冒泡结束之后就会得到一个最大的值。先来实现一轮的比较

for(int j = 0; j < a.length - 1; j ++) {
  if(a[j] > a[j+1]) {
    swap(a, j, j+1);
  }
}
复制代码

我们需要比较多少轮呢?数组有多少个元素就比较多少轮。再添加一个外层循环即可。比较简单对吧,我们看一下基础版的实现。

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

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
复制代码

这里内层循环控制的是每轮的比较(一共比较了 a.length - 1 次,后面就是记作 n),外层控制的是多少个元素的冒泡。这里应该可以发现一个小小的问题了,是不是每一个元素都需要比较 n 次呢,显然是不需要的,第一次冒泡的时候已经把最大的元素放在了最后,第二轮比较的时候只需要比较 n - 1 次即可,依次类推。我们就可以这样优化内层的比较。

      for (int j = 0; j < a.length - 1 - i; j++) {
          if(a[j] > a[j+1]) {
              swap(a, j, j+1);
          }
      }
复制代码

上面的优化还比较好理解,我们再思考一下如何优化,优化无非就两个方面,一个是减少冒泡的次数,另一个就是减少每次冒泡的比较次数。

现在思考一个极端的场景,给定的数组就是有序的,此时会出现什么情况呢?外层一直在循环,内层其实一次 swap 都没有调用,但是,问题是,如果在第一轮的两两比较中都没有发生交换,其实数组就已经是有序的了,我们不需要外层一直循环了。

算法实现优化版

    private static void bubble(int[] a) {
        for (int i = 0; i < a.length; i++) {
            boolean swapped = false;
            for (int j = 0; j < a.length - 1 - i; j++) {
                if(a[j] > a[j+1]) {
                    swap(a, j, j+1);
                    swapped = true;
                }
            }
            if(!swapped) {
                break;
            }
        }
    }
    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }    
复制代码

到这里看起来已经非常棒了,上面我们已经对冒泡的次数,以及每次冒泡需要比较的次数都做了优化,但这就够了嘛,不 我们还可以对比较的次数做进一步的优化,每轮冒泡时,最后一次交换的索引可以作为下一轮冒泡比较的次数,如果这个值是 0, 就证明整个数组有序,直接退出外层循环即可。

换句话说,我们要不要继续冒泡,取决内层的循环,如果一次都没有交换,那就表明数组有序,直接退出即可,如果内层循环只交换了一次,那么这个交换的位置就是我们下一次冒泡需要比较的次数,如果交换了两次,那我们就取后面的位置作为下一次冒泡比较的次数,不知道这样的解释大家能不能听得懂。

举个例子吧,a = [5,3,1,9] 第一次冒泡之后的结果:3159, 最后交换的位置 j 等于 2 下一次冒泡时只需要比较 2 次即可。我理解这个也花了一会儿,但是仔细想想还是有点感觉的,因为我上一轮若是没有交换,就说明后面的都是有序的,自然下一轮也不需要再去比较了。上一轮冒泡比较到哪里,下一轮的冒泡就比较到那里即可。上一轮冒泡没有比较,证明数组有序,直接退出。

算法实现终极版

    private static void bubble(int[] a) {
        int loopCount = a.length - 1;
        while(true) {
            int lastIndex = 0;
            for (int j = 0; j < loopCount; j++) {
                if(a[j] > a[j+1]) {
                    swap(a, j, j+1);
                    lastIndex = j;
                }
            }
            if(lastIndex == 0) {
                break;
            }
        }
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
复制代码

综上,希望我已经把冒泡排序说的清清楚楚了,如果你有更好的解法,欢迎留言讨论。

猜你喜欢

转载自juejin.im/post/7016327235086843934