排序算法(上):冒泡、选择、插入、希尔

排序,是嵌入式开发里躲不开的基本功。

我刚开始写单片机程序时,总觉得排序是“上层的事”。直到有一次做数据采集系统,传感器一口气吐了200个乱序的采样值,我得在中断里快速把它们排好,再送出去显示。那时候我才意识到——排序算法选不对,系统响应时间直接崩掉。

今天咱们就把四种基础排序算法讲透。它们虽然简单,但理解透了,后面学快排、堆排序才会顺手。


1. 冒泡排序:最直观,但最慢

冒泡排序的思路,说白了就是“大的往后沉”。

每一轮从头到尾比较相邻两个元素,如果前一个比后一个大,就交换。一轮下来,最大的数就“冒”到了最后。然后下一轮忽略最后一个,继续。

核心思想: 每一轮把未排序部分的最大值“冒”到末尾。

C语言实现

void bubble_sort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int swapped = 0;  // 优化标志
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
                swapped = 1;
            }
        }
        if (!swapped) break;  // 没交换,说明已经有序
    }
}
我的小技巧: 加一个 swapped 标志位。如果某一轮没有发生任何交换,说明数组已经有序,直接跳出。我在做低功耗设备时,经常用这个优化来减少不必要的循环。

复杂度分析

情况时间复杂度空间复杂度
最好(已有序)O(n)O(1)
最坏(逆序)O(n²)O(1)
平均O(n²)O(1)
注意: 冒泡排序是稳定排序。但它的交换次数太多,在数据量大时效率很低。我一般只在数据量小于50时才会考虑用它。

2. 选择排序:简单直接,但不稳定

选择排序的思路更“暴力”——每一轮找到未排序部分的最小值,直接放到最前面。

你想想看,它不像冒泡那样频繁交换,而是每轮只交换一次。但代价是,它没法提前结束,不管数据多有序,都得跑完 n-1 轮。

C语言实现

void selection_sort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int min_idx = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[min_idx]) {
                min_idx = j;
            }
        }
        if (min_idx != i) {
            int tmp = arr[i];
            arr[i] = arr[min_idx];
            arr[min_idx] = tmp;
        }
    }
}
避坑指南: 我曾经在项目中用选择排序处理结构体数组,结果发现排序后相同 key 的元素的相对顺序变了。因为选择排序是不稳定的——它会把后面的小元素直接换到前面,打乱相同值的顺序。如果你需要稳定排序,别选它。

复杂度分析

情况时间复杂度空间复杂度
最好O(n²)O(1)
最坏O(n²)O(1)
平均O(n²)O(1)

嗯,这里要注意:选择排序的时间复杂度是“铁板一块”,不管数据长什么样,都是 O(n²)。所以它只适合数据量极小、且不要求稳定性的场景。


3. 插入排序:小数据量的王者

插入排序的思路,很像你打扑克牌时整理手牌——每次摸一张新牌,插入到已经排好序的手牌中正确的位置。

我个人非常喜欢插入排序。为什么?因为它在数据基本有序时,效率高得吓人。而且它是稳定排序,实现也简单。

C语言实现

void insertion_sort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}
关键点: 插入排序是“边比较边移动”,而不是先比较再交换。这样减少了赋值次数。

复杂度分析

情况时间复杂度空间复杂度
最好(已有序)O(n)O(1)
最坏(逆序)O(n²)O(1)
平均O(n²)O(1)
我的经验: 在嵌入式项目中,如果数据量小于100,我几乎无脑选插入排序。它比冒泡快,比选择排序稳定,而且代码量小。有一次做实时数据记录器,我甚至用它来维护一个“最近10个最大值”的有序列表,效果非常好。

4. 希尔排序:插入排序的“升级版”

希尔排序,说白了就是“分组插入排序”。

它先把数组按一定间隔(gap)分成若干子序列,对每个子序列做插入排序。然后缩小 gap,重复这个过程,直到 gap=1 时做一次完整的插入排序。

为什么要这么做?因为插入排序在数据基本有序时效率很高。希尔排序通过“大步跳跃”的方式,让数据快速接近有序状态。

C语言实现

void shell_sort(int arr[], int n) {
    // 使用 Hibbard 增量序列:1, 3, 7, 15, ...
    for (int gap = n / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < n; i++) {
            int key = arr[i];
            int j = i;
            while (j >= gap && arr[j - gap] > key) {
                arr[j] = arr[j - gap];
                j -= gap;
            }
            arr[j] = key;
        }
    }
}
注意: 希尔排序的增量序列选择会影响性能。上面用的是最简单的 gap = n/2, n/4, ... 1。但实际中,Hibbard 增量(2^k - 1)或 Sedgewick 增量效果更好。我建议你根据数据规模选合适的增量。

复杂度分析

增量序列时间复杂度(平均)空间复杂度
n/2 递减O(n²)(最坏)O(1)
Hibbard 增量O(n^(3/2))O(1)
Sedgewick 增量O(n^(4/3))O(1)
避坑指南: 我曾经在项目中用希尔排序处理一个 5000 个元素的数组,用的 gap = n/2 递减。结果发现最坏情况下它退化成了 O(n²),比插入排序还慢。后来换成 Hibbard 增量,性能明显提升。所以——增量序列不是随便选的。

5. 四种排序对比总结

算法最好最坏平均空间稳定适用场景
冒泡排序O(n)O(n²)O(n²)O(1)稳定数据量极小(<50)
选择排序O(n²)O(n²)O(n²)O(1)不稳定数据量小,不要求稳定
插入排序O(n)O(n²)O(n²)O(1)稳定数据量小或基本有序
希尔排序O(n log n)O(n²)~O(n^(4/3))取决于增量O(1)不稳定中等规模数据(<5000)

我个人建议:

  • 数据量 < 50:用插入排序,简单稳定。
  • 数据量 50~5000:用希尔排序,选好增量序列。
  • 数据量 > 5000:别用这四种了,后面会讲快速排序和归并排序。

6. 知识体系图

下面这张图帮你理清这四种排序的关系和选择逻辑:

排序算法(上)知识体系 四种基础排序 冒泡排序 选择排序 插入排序 希尔排序 稳定 | O(n²) 不稳定 | O(n²) 稳定 | O(n²) 不稳定 | O(n^(3/2)) 适用:数据量极小 (<50) 适用:不要求稳定 数据量小 适用:基本有序 或数据量小 适用:中等规模 (<5000) 选择建议:小数据用插入,中等数据用希尔 下一章:快速排序、归并排序、堆排序

好了,这四种排序算法就讲到这里。它们虽然基础,但理解透了,后面学高级排序会轻松很多。记住:没有最好的算法,只有最适合场景的算法。下次写排序代码前,先问问自己——数据有多大?是否基本有序?要不要稳定?想清楚再动手。

一句话总结: 冒泡最直观,选择最简单,插入最实用,希尔最灵活。

公众号:蓝海资料掘金营,微信deep3321