vlambda博客
学习文章列表

十大排序算法之冒泡排序

从我接触算法开始,就听说排序算法是基础中的基础,但是我直到现在也记不住很多算法的核心理念,所以准备再重新复习一次并记录下来。


首先附上一张经典图(网上找的),里面包含了各个算法是否稳定和相关的复杂度详情。

这篇文章我们主要介绍第一个稳定的排序算法--冒泡排序,其他的序算法会在接下来的文章中依次介绍。


什么是稳定排序? 

假设a=b, a原本在b的前面,那么在排序后a依然在b的前面。


冒泡排序(Bubble Sort)

一种比较简单无脑的排序算法。

  1. 从头开始比较两个相邻的元素,如果前面的大于右面的,则交换位置

  2. 依次比较每对元素,这样每一次遍历数组的最后一个数字是最大值

  3. 针对所有的元素重复以上的步骤,PS: 不用比较已经排序好的元素;

  4. 重复步骤1~3,直到排序完成。


那么下面用代码实现一下,

“Talk is cheap. Show me the code.”


/**     * 暴力实现,遍历所有元素,不考虑已经排序好的元素 * @param nums 无序数组, nums.length >= 2 */ public static void bubbleSort(int[] nums) { for (int i = 0; i < nums.length - 1; i++) {            //一定要注意这层循环的index不要写错, 我第一次就写成j=i了。。 for (int j = 0; j < nums.length - 1; j++) { if (nums[j] > nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; } } } }

上面这个写法虽然通俗易懂,但是数据量大或者特殊数组确实做很多无用功。

因为每一次排序当前比较的最大值都已经依次排在了倒数第一,倒数第二....


那么如何优化呢?

最容易想到的就是我们每次循环已经把最大值放到末尾了,我们可以修改循环条件,那么接下来的循环就不用再继续比较末尾已经排好序的值了。(很多人看答案可能直接会写最优解,但是刷题我个人感觉你还是要follow自己思想,毕竟你面试的时候可能一下子想不到最优解。。。。)

/** * 优化尾部index, 已经排序好的元素不需要再比较第二次 * @param nums 无序数组, nums.length >= 2 */ public static void bubbleSort1(int[] nums) { for (int i = 0; i < nums.length - 1; i++) {            //一定要注意这层循环的index不要写错, 我第一次就写成j=i了。。 for (int j = 0; j < nums.length - 1 - i; j++) { if (nums[j] > nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; } } } }


为什么循环条件要写成length-1-i呢, 我们要清楚第一层循环i的含义是什么。i就代表了当前数组中已经排序好的数的总数,i=0时我们刚开始进行排序,那么数组中没有排好序的数,所以length-1-i=length-1;i=1时说明我们数组的最后一个数已经是最大值了,所以第二层循环时就不需要再进行比较了,以此类推。。


PS: 可能大佬会感觉我这里说了太多了,但是很多时候有些人就是理解不了为什么要写成length-1-i,如果已经理解了就直接跳过吧哈哈


这种优化写完之后,我们又想到,如果是那种特殊数组呢?例如:[100,1,2,3,4,5,6,7],明明只需要一次排序就可以了但是我们的代码还是要做很多无用功。我们知道冒泡排序完成的标志就是在一次排序中没有元素的位置发生改变,所以我们可以加入一个flag来记录当前循环是否有元素位置发生改变,没有的话可以直接终止循环。

/** * 加入flag,如果没有元素位置改变则终止循环 * @param nums 无序数组, nums.length >= 2 */ public static void bubbleSort2(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {            //每当新循环开始重置这个值,只要boolean值没发生改变就终止循环 boolean isSorted = true; //一定要注意这层循环的index不要写错, 我第一次就写成j=i了。。 for (int j = 0; j < nums.length - 1 - i; j++) { if (nums[j] > nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; isSorted = false; } } if(isSorted) break; } }

逻辑简单易懂,就不多加解释了。


讲道理到这我感觉我已经完成优化了,然后上网查了查大佬们的写法,又发现一个优化方法(不解释,膜拜就完了)


优化方法是记录下来每次lastExchange的index,这样区间nums[lastExchange, length-1]就是有序区,nums[0, lastExchange-1]是无序区,因此记住最后一次交换发生的位置lastExchange,从而减少排序的趟数。

 /** * 加入lastExchange,通过此来减少排序的次数 * @param nums 无序数组, nums.length >= 2 */ public static void bubbleSort3(int[] nums) {
int lastExchange = 0, unSortedLength = nums.length - 1;
for (int i = 0; i < nums.length - 1; i++) { boolean isSorted = true; //一定要注意这层循环的index不要写错, 我第一次就写成j=i了。。 for (int j = 0; j < unSortedLength; j++) { if (nums[j] > nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; isSorted = false; //这里记录lastExchange的元素index                    //一定是j而不是j+1,因为nums[j+1]已经是排序完成的数 lastExchange = j; } } if (isSorted) { break; }            //更新第二层循环的范围,只需要排序无序区即可 unSortedLength = lastExchange; } }

这里加入了新的参数unSortedLength lastExchange 用来记录我们最后一次更新位置的索引,把最后一次更改的元素位置赋值给lastExchange ,然后更新第二层循环的排序次数,在下一次新的循环时只需要排序[0, unSortedLength ]区域即可。

PS: 有人问为什么不同时更新第一层循环的size呢?因为我们已经加入了flag来判断是否有元素位置改变,如果没有的话直接就会跳出循环,所以最外层并不需要修改。


从上面的解法我们也可以看出,每一次优化的代码都是在基础的暴力解法上一点点进行修改的,就算是最后一种lastExchange的解法,我们前面的flag也是结合在一起使用。可能题做得多了一下子就能想到最后一种解法,这里只是提供了我在解题时的大脑活动。


空间复杂度:

空间复杂度就是在交换元素的时候那个temp参数占用的内存:

  1. 如果初始数组有序,那么是空间复杂度最优:0;

  2. 如果初始数组逆序,那么空间复杂度最差: O(n)

  3. 平均复杂度 O(1)


PS:平均复杂度好像需要数学公式去算,说真的就10个排序算法,平均复杂度和最差空间复杂度就背下来吧

时间复杂度: 

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

复杂度要根据你的具体代码来进行计算,例如你面试的时候就写出了第一种暴力解法,那没得说,最优和最差复杂度就是O(n^2),平均复杂度也是O(n^2),这里我们使用我们的最优解来计算复杂度

  1. 最坏O(n^2), 例如[7,6,5,4,3,2,1]这样的数组是需要运行所有的

  2. 最好O(n), 为什么和暴力解法不一样呢, 假设我们的数组初始情况下就已经是有序的了,那么我们在第一次排序时之只循环一次数组,flag没有更改直接就退出循环了,所以是O(n),

    但是如果没有使用flag等优化,就是O(n^2)。

  3. 平均复杂度:O(n^2)