冒泡排序 (Bubble Sort)

31 阅读5分钟

1. 原理解析

生活中的例子: 想象一下,体育课上老师让大家按照身高从低到高排成一队。同学们随意站成了一排。 老师说:“从队首开始,相邻的两个人互相比身高,如果前面的人比后面的人高,你们俩就交换位置。比完一轮后,最高的那个人一定会站到队尾。” 这就叫冒泡排序。像水里的气泡一样,每次比较,最大的元素都会慢慢“浮”到最后面。

算法核心步骤

  1. 每次只比较相邻的两个元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。每一轮结束后,当前未排序部分的最大元素会被放置到它应该在的位置(队尾)。
  3. 针对所有的元素重复以上的步骤。随着轮数的增加,需要参与比较的元素会越来越少(因为后面的大元素已经排好了)。
  4. 持续重复上面的步骤,直到没有任何一对数字需要比较。

2. 使用场景

在实际的工程开发中,冒泡排序由于其 O(n2)O(n^2) 的时间复杂度,几乎不会被用于处理大量数据的排序(工业界通常会用快速排序、归并排序或语言内置的排序方法)。

但是,冒泡排序依然有它的重要价值:

  1. 教学与面试:它是理解“交换排序”和“算法优化(如提前退出)”的最佳入门素材。面试中常用来考察候选人对基础边界条件和优化的掌握。
  2. 数据量极小且基本有序:如果你的数据量非常小(比如十几个),并且大部分已经排好序了,冒泡排序(特别是带 swapped 优化的版本)跑起来非常快,甚至能达到接近 O(n)O(n) 的效率。它代码实现简单,没有额外函数调用的开销。

3. 代码实战

Java 实现

public class BubbleSort {
    public static void sort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int n = arr.length;
        // 外层循环控制比较的轮数
        for (int i = 0; i < n - 1; i++) {
            // 提前退出的标志位,优化算法
            boolean swapped = false;
            // 内层循环控制每轮比较的次数
            for (int j = 0; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    // 交换 arr[j] 和 arr[j+1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                }
            }
            // 优化:如果某一轮一次交换都没发生,说明数组已经完全有序,提前结束
            if (!swapped) {
                break;
            }
        }
    }
}

Python 实现

def bubble_sort(arr):
    n = len(arr)
    if n < 2:
        return arr
        
    # 外层循环控制比较的轮数
    for i in range(n - 1):
        swapped = False
        # 内层循环控制每轮比较的次数
        for j in range(n - 1 - i):
            if arr[j] > arr[j + 1]:
                # Python 里的变量交换非常简洁
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        
        # 优化:如果没有发生交换,提前结束
        if not swapped:
            break
            
    return arr

4. 核心难点:循环边界详解

对于初学者来说,代码中最容易写错的地方就是两层循环的边界:n - 1n - 1 - i。下面详细拆解:

为什么第一层(外层)循环是 n - 1 次?

外层循环代表的是**“排好一个最大值需要走几轮”。

直白的例子: 假设你们班只有 3 个同学(n=3)排身高。

  • 第 1 轮比较完,全班第 1 高的同学站到了最后面。
  • 第 2 轮比较完,剩下的两人里较高的那个(也就是全班第 2 高)站到了倒数第二的位置。

这时候,还剩下最后 1 个同学站在队首。他还需要再找人比吗?不需要了,因为比他高的都排到后面去了,他自然就是全班最矮的。 所以,如果有 n 个人,只要排好 n - 1 个人,剩下那个自动就在正确的位置上。这就是为什么外层循环只需要走 n - 1 轮。

为什么第二层(内层)循环是 n - 1 - i 次?

内层循环代表的是**“在当前这一轮里,需要做多少次相邻两人的比较”。

这里的关键在于:每走完一轮,队伍最后面就会多出一个已经排好序的“最高个”,下一轮就不需要再去管他们了

  • n - 1 的部分: 因为我们每次是拿当前的人 arr[j] 和他后面的人 arr[j+1] 比。为了防止数组越界(不能拿最后一个人和空气比),所以比较的起点 j 最多只能走到倒数第二个人,也就是 n - 2 的位置。在代码里写成 j < n - 1

  • - i 的部分(精髓所在): 这里的 i 就是已经过去了几轮i 从 0 开始)。

    • i = 0(第 1 轮):没有任何人排好,你需要从头到尾比一遍,所以比较次数是 n - 1 - 0。结束后,第 1 高的人固定在最后了。
    • i = 1(第 2 轮):最后 1 个人已经是最高的了,他不需要再参与比较。所以你要比的人少了一个,比较次数变成 n - 1 - 1
    • i = 2(第 3 轮):最后 2 个人已经排好了,不需要再参与。比较次数变成 n - 1 - 2

总结- i 就是为了剔除掉后面已经排好序的那些人,避免做无用功。如果不减 i,每次都要和最后面已经站好位置的“高个子”们再比一遍,会白白浪费性能。


5. 复杂度分析

衡量一个算法好坏的核心标准是它的复杂度。

  • 时间复杂度(Time Complexity)

    • 最坏情况:数组完全倒序,需要比较和交换 n(n1)/2n(n-1)/2 次,时间复杂度为 O(n2)O(n^2)
    • 最好情况:数组已经是有序的。因为代码中加入了 swapped 标志位优化,只需要遍历一次数组,没发生交换就直接退出,时间复杂度为 O(n)O(n)
    • 平均情况O(n2)O(n^2)
  • 空间复杂度(Space Complexity)

    • 由于所有的交换都是在原数组上直接进行的(就像同学们只是在操场上换个位置,不需要去另外的空地排队),只用了几个临时变量记录状态。所以空间复杂度为 O(1)O(1)。这在算法里叫做原地排序(In-place sort)