冒泡排序动图讲解(JS代码实现及优化)

679 阅读6分钟

什么是冒泡排序?

冒泡排序的英文Bubble Sort,是一种最基础的交换排序。之所以叫做冒泡排序,因为每一个元素都可以像小气泡一样,根据自身大小一点一点向数组的一侧移动。

冒泡排序的原理:

每一趟只能确定将一个数归位。 即第一趟只能确定将末位上的数归位,第二趟只能将倒数第 2 位上的数归位,依次类推下去。如果有 n 个数进行排序,只需将 n-1 个数归位,也就是要进行 n-1 趟操作。【外层循环

“每一趟 ” 都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后向后挪一位继续比较下面两个相邻的两个数大小关系,重复此步骤,直到最后一个还没归位的数。【内层循环

冒泡排序到底是如何排序的呢?

下面通过一个动图来看一看冒泡排序到底是怎么移动的

冒泡排序动图.gif

从以上动图可以清楚看到,冒泡排序每一趟只能确定将一个数归位。即第一趟只能确定将末位上的数归位,第二趟只能将倒数第 2 位上的数归位,依次类推下去。

具体是如何移动的呢?

(1)起始时,左下标指向第一个石子,右下标指向第二个石子,然后比较

image.png

(2)然后左右下标同时向右移动,再次比较

image.png

(3)第一趟结束之后,最大的石子就移动到了最右边

image.png

(4)接下来就从剩下 3 个没排好序的石子中继续选出最大的,规则和上面一样

image.png

(5)下面给出这 4 个石子完整的演示过程

image.png

时间复杂度

由上图可知,4n=4) 个石子的时候排完序需要 3n-1第一趟需要比较3n-1次,第二趟需要比较2n-2次,第三趟需要比较1 (n-3) ,那一共比较了 3 + 2 + 1 次;

那如果有 n 个石子呢?

那就需要 (n-1) + (n-2) +…+2+1 次,这不就是一个等差数列嘛!很显然:

image.png

根据复杂度的规则,去掉低阶项(也就是 n/2),并去掉常数系数,那时间复杂度就是 O(n^2) 了 ;

冒泡排序是一种稳定排序,因为在两个数交换的时候,如果两个数相同,那么它们并不会交换位置。

所谓稳定性,其实就是说,当你原来待排的元素中间有相同的元素,在没有排序之前它们之间有先后顺序,在排完后它们之间的先后顺序不变,我们就称这个算法是稳定的。

image.png

js实现冒泡排序原始版本V1

// 从小到大排序
function bubbleSort(arr) {
  // 外层循环,确定需要多少趟(即i的限制条件),数组中有n个元素,需要排 n-1 趟,其中 n = arr.length
  for (let i = 0; i < arr.length - 1; i++) { // i从 0 到 n-2, 也就是循环了n-1次, 所以代码中限制为 i < n-1
    // 内层循环,确定每趟需要比较多少次(即j的限制条件),因为每一趟都能确定1个数的顺序,循环i次之后就确定了i个数的顺序,这i个数不用再参加比较,因此每一趟需比较 n-i 个数,即比较 n-i-1 次,代码中j从0开始,所以限制 j < n-1-i
    // 因为j为左侧元素的索引,所以从索引上考虑也容易想清楚,每一趟需要参与比较的最后一个元素的索引为n-1-i, 所以右侧元素索引最多为n-1-i,即j+1<=n-1-i, j<=n-2-i, 即j<n-1-i
    for (let j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) { // 若左侧元素大于右侧元素,则交换位置
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }  //左侧元素小于或等于右侧元素,位置保持不变
    }
  }
  return arr;
}

确定内层循环的比较次数,可以参考下图

image.png

image.png

第1趟(i=0),比较次数n-1-i次,即4-1-0

第2趟(i=1),比较次数n-1-i次,即4-1-1

随着趟数的增加,比较的次数也随之减小。

性能优化

我们先来看一个例子:

640?wx_fmt=png

对数列 5,8,6,3,9,2,1,7 进行排序

使用 上述 原始版本代码v1 对数组 [5,8,6,3,9,2,1,7] 排序,用时0.274ms

arr_test = [5,8,6,3,9,2,1,7];
console.time("bubbleSort");
bubbleSort(arr_test);
console.timeEnd("bubbleSort");
console.log(arr_test);

image.png

当按照上述代码分别执行到第六、第七、第八轮的时候,数列状态如下:

640?wx_fmt=png

很明显可以看出,自从经过第六轮排序,整个数列已然是有序的了。可是我们的排序算法仍然“兢兢业业”地继续执行第七轮、第八轮。

这种情况下,如果我们能判断出数列已经有序,并且做出标记,剩下的几轮排序就可以不必执行,提早结束工作。

代码优化也很简单,将上面代码做一点点小小改动即可,也就是利用布尔变量 isSorted作为标记。如果在本轮排序中,元素有交换,则说明数列无序;如果本轮一直都没有元素交换,说明数列已然有序,直接跳出大循环。

冒泡排序优化版本v2

// 优化外层循环,减少排序的趟数,但内层循环仍是兢兢业业跑完的,因此可以继续优化
function bubbleSort(arr) {
  // 外层循环
  for (let i = 0; i < arr.length - 1; i++) {
    let isSorted = true;  //有序标记,每一趟初始值都是true
    for (let j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
        isSorted = false  // 只要发生交换,说明数组不是有序的,将isSorted置为false
      }
    }
    // 完成一趟比较后,若没有发生任何交换,说明数组已经排好序了,无需再进行下一趟排序,跳出外层循环即可。
    if (isSorted) {
      break;
    }
  }
  return arr;
}

使用上述 优化版本代码v2 对数组 [5,8,6,3,9,2,1,7] 排序,用时0.225ms

image.png

冒泡排序进一步优化版本v3

如果数列中前半部分是无序的,后半部分是有序的,比如这个数组

image.png

前半部分 3,4,2,1 无序,后半部分 5,6,7,8 升序,并且后半部分的元素已经是数列最大值。

即使是上述已经优化的代码版本,针对这类数组进行每一轮排序时,还是白白比较了许多次,这正是另一个需要优化的点。

function bubbleSort(arr) {
  let lastExchangeIndex;  // 记录最后一次交换的位置
  let sortedBorder = arr.length - 1; // 无序数列的边界,每次比较只需要比到这里为止。
  for (let i = 0; i < arr.length - 1; i++) {
    let isSorted = true;  //有序标记,每一趟初始值都是true
    for (let j = 0; j < sortedBorder; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
        isSorted = false  // 只要发生交换,说明数组不是有序的,将isSorted置为false
        lastExchangeIndex = j;  // 更新为发生交换时左侧元素的索引
      }
    }
    sortedBorder = lastExchangeIndex; // 完成一趟比较后,记录边界索引值。
    // 完成一趟比较后,若没有发生任何交换,说明数组已经排好序了,无需再进行下一趟排序,跳出外层循环即可。
    if (isSorted) {
      break;
    }
  }
  return arr;
}

这一版代码中,sortedBorder就是无序数列的边界。每一轮排序过程中,sortedBorder之后的元素就完全不需要比较了,肯定是有序的。

对于没有这种规律的普通数组 [5,8,6,3,9,2,1,7 ] 来说,v3用时比v1少,然而并没有v2用时少。

使用上述 优化版本代码v3 对数组 [5,8,6,3,9,2,1,7] 排序,用时0.261ms

image.png

而对于部分有序的特殊数组 [3,4,2,1,5,6,7,8] 来说,v3用时明显减少很多。

image.png

image.png

image.png

参考

# 漫画:什么是冒泡排序?

# 冒泡排序(超详细)

# 要是你还看不懂这篇冒泡排序,麻烦找我要红包