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

排序,是嵌入式开发里最基础也最常用的操作之一。你想想看,从传感器采集到的数据要排序,任务调度队列要排序,通信协议里的报文也要排序。我个人习惯把排序算法分成两类:简单排序和高级排序。今天咱们先聊四个简单但非常实用的——冒泡、选择、插入和希尔排序。

1. 冒泡排序:最直观的“泡”法

冒泡排序的思路特别简单:从头到尾,两两比较,把大的往后“冒”。每一轮下来,最大的数就像气泡一样浮到末尾。

我在项目中遇到过一个小问题:用冒泡排序处理一个256字节的数组,结果在8位MCU上跑了将近半秒。嗯,数据量小的时候还行,大了就扛不住了。

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 temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = 1;
            }
        }
        if (!swapped) break;  // 没交换说明已经有序
    }
}
小技巧:加一个 swapped 标志位,如果某一轮没有发生交换,说明数组已经有序,直接跳出。这个优化在数据接近有序时效果很明显。

时间复杂度:最好 O(n),最坏 O(n²),平均 O(n²)。空间复杂度 O(1)。

2. 选择排序:每次挑最小的

选择排序的思路更直接:每一轮从剩余元素里挑出最小的,放到前面。说白了就是“矮子里拔将军”,一轮一个。

我曾经用选择排序给一个按键扫描的去抖数据排序,因为数据量只有10个左右,代码简单,不容易出错。但要注意,选择排序是不稳定的——相同值的元素可能会交换位置。

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 temp = arr[i];
            arr[i] = arr[min_idx];
            arr[min_idx] = temp;
        }
    }
}
注意:选择排序无论数据是否有序,比较次数都是固定的 n(n-1)/2 次。所以它不适合对“接近有序”的数据排序——冒泡或插入会快得多。

时间复杂度:最好/最坏/平均都是 O(n²)。空间复杂度 O(1)。

3. 插入排序:像打牌一样理牌

插入排序的思路,你打牌时肯定用过:摸到一张新牌,插入到手里已经排好序的牌中。从第二个元素开始,把它插入到前面有序序列的合适位置。

我个人特别喜欢插入排序,因为它在数据量小(比如几十个)或者数据基本有序时,效率非常高。我在做实时系统任务调度时,经常用它来维护一个按优先级排序的小队列。

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(n²),平均 O(n²)。空间复杂度 O(1)。

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

希尔排序,说白了就是给插入排序加了一个“预处理”步骤。它先把数组按一定间隔分组,对每组进行插入排序,然后逐步缩小间隔,直到间隔为1时做一次完整的插入排序。

为什么这样能快?因为插入排序在数据“基本有序”时效率很高。希尔排序通过前期的大间隔排序,让数据快速接近有序状态。

我记得有一次在ARM Cortex-M3上做数据采集,数据量大概500个,用插入排序要跑好几毫秒,换成希尔排序后直接降到不到1毫秒。嗯,差距就是这么明显。

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 temp = arr[i];
            int j;
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap];
            }
            arr[j] = temp;
        }
    }
}
增量选择:我常用的增量序列是 gap = n/2, n/4, ..., 1(Shell原始序列)。但如果你追求更优性能,可以试试 Hibbard 序列(2^k - 1)或 Sedgewick 序列。不同的增量会影响时间复杂度,从 O(n^(3/2)) 到 O(n^(4/3)) 不等。

5. 四种排序对比

算法 最好时间 最坏时间 平均时间 空间 稳定性 适用场景
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定 数据量小,或接近有序
选择排序 O(n²) O(n²) O(n²) O(1) 不稳定 数据量小,对稳定性无要求
插入排序 O(n) O(n²) O(n²) O(1) 稳定 数据量小,或数据基本有序
希尔排序 O(n log n) O(n²) O(n^(1.3~2)) O(1) 不稳定 中等数据量,嵌入式常用

6. 知识体系图

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

排序算法选择指南 数据量多大? 小数据量(< 100) 中等数据量(100~1000) 接近有序? → 插入排序 无序? → 选择排序 内存紧张? → 希尔排序 内存充足? → 快速排序(后续) 嵌入式实战建议 数据量 < 50:插入排序 | 50~500:希尔排序 | > 500:考虑快速排序或归并排序

7. 避坑指南

最后,分享几个我踩过的坑:

  • 冒泡排序的优化陷阱:我曾经以为加了 swapped 标志就万事大吉,结果忘了在每轮开始时重置它。嗯,bug 找了一下午。
  • 选择排序的不稳定性:有一次给结构体数组排序,按某个字段排序后,相同字段的原始顺序被打乱了。如果你需要稳定排序,别用选择排序。
  • 希尔排序的增量选择:增量序列选不好,性能可能还不如插入排序。我一般用 gap = n/2 开始,简单够用。
  • 嵌入式环境下的栈空间:递归排序(比如快速排序)在栈小的 MCU 上容易爆栈。希尔排序是迭代的,没有这个问题,所以我经常用它。

这四种排序是基础中的基础。你想想看,掌握了它们,再学快速排序、归并排序就会轻松很多。下一节咱们继续聊更高级的排序算法。


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