排序进阶:选择排序、归并排序、基数排序

各位同学,今天我们聊点硬核的。前面我们讲完了插入排序、交换排序这些基础款,说实话,它们在小规模数据下表现还行,但一旦数据量上来,性能就有点捉襟见肘了。今天我们要讲的这三种排序——选择排序的升级版、归并排序、基数排序,才是真正能在生产环境中扛大梁的角色。

我个人习惯把排序算法分成两类:一类是“比较排序”,一类是“非比较排序”。今天这三种里,前两种是比较排序,基数排序则另辟蹊径。咱们一个一个来。

一、简单选择排序——最朴素的“挑最小”

简单选择排序的思路,说白了就是“每次从剩下的元素里挑一个最小的,放到前面去”。你想想看,这就像你在一堆扑克牌里,一张一张找最小的那张,找到就放到手里,然后继续找剩下的里面最小的。

代码实现非常直观:

void selectSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int minIdx = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIdx]) {
                minIdx = j;
            }
        }
        if (minIdx != i) {
            int tmp = arr[i];
            arr[i] = arr[minIdx];
            arr[minIdx] = tmp;
        }
    }
}

嗯,这里要注意:简单选择排序的时间复杂度是 O(n²),不管数据原本是什么顺序,它都得老老实实比较 n(n-1)/2 次。我在项目中遇到过有人用它来排几万条数据,结果等了半天没反应——这就是没算清楚复杂度。

避坑指南:我曾经接手过一个嵌入式项目,里面用简单选择排序处理传感器采集的实时数据流。数据量其实不大,但每次排序都会阻塞主循环。后来我改成只在数据缓冲区满时才排序,才解决了问题。记住:简单选择排序适合小数据量,别在实时性要求高的场景里频繁调用。

二、堆排序——用二叉树思想加速选择

简单选择排序慢在哪?慢在每次找最小值都要遍历整个剩余数组。那有没有办法能快速找到最小值?有,用堆。

堆排序的核心思想:先把数组构建成一个大顶堆(或小顶堆),然后每次把堆顶元素(最大值或最小值)和末尾元素交换,再调整堆。这样每次取最值的时间复杂度就从 O(n) 降到了 O(log n)。

看代码:

void heapify(int arr[], int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;

    if (left < n && arr[left] > arr[largest])
        largest = left;
    if (right < n && arr[right] > arr[largest])
        largest = right;

    if (largest != i) {
        int tmp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = tmp;
        heapify(arr, n, largest);
    }
}

void heapSort(int arr[], int n) {
    // 建堆
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);
    // 排序
    for (int i = n - 1; i > 0; i--) {
        int tmp = arr[0];
        arr[0] = arr[i];
        arr[i] = tmp;
        heapify(arr, i, 0);
    }
}

堆排序的时间复杂度是 O(n log n),而且它是在原地排序,不需要额外空间。这一点在实际嵌入式开发中非常实用——内存就那么点,能省则省。

个人经验:我在做车载系统时,需要定期对 CAN 总线上的报文按优先级排序。堆排序的稳定 O(n log n) 表现让我很放心,而且它不需要递归调用,避免了栈溢出的风险。如果你也在做资源受限的系统,堆排序是个好选择。

三、归并排序——分而治之的典范

归并排序的思路很优雅:把数组分成两半,分别排序,然后合并。你想想看,如果两个子数组都已经有序了,合并起来就很简单——像两叠按大小排好的扑克牌,一张一张比大小拿就行了。

代码实现:

void merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;
    int L[n1], R[n2];

    for (int i = 0; i < n1; i++) L[i] = arr[left + i];
    for (int j = 0; j < n2; j++) R[j] = arr[mid + 1 + j];

    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) arr[k++] = L[i++];
        else arr[k++] = R[j++];
    }
    while (i < n1) arr[k++] = L[i++];
    while (j < n2) arr[k++] = R[j++];
}

void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}

归并排序的时间复杂度也是 O(n log n),但它需要 O(n) 的额外空间来存储临时数组。这一点在嵌入式开发中要特别注意——如果你的系统内存只有几十 KB,归并排序可能就不太合适了。

关键点:归并排序是稳定的排序算法。什么意思?就是相等的元素在排序后相对位置不变。我在做数据库索引维护时,这个特性非常关键——因为索引需要保持记录的原始顺序。

四、基数排序——不比较,直接分桶

前面讲的排序都是通过比较元素大小来排序的。基数排序不一样,它按位来排。比如对三位数排序,先按个位分桶,再按十位分桶,最后按百位分桶。每次分桶后收集起来,最终就是有序的。

看代码:

void radixSort(int arr[], int n) {
    int max = arr[0];
    for (int i = 1; i < n; i++)
        if (arr[i] > max) max = arr[i];

    for (int exp = 1; max / exp > 0; exp *= 10) {
        int output[n];
        int count[10] = {0};

        for (int i = 0; i < n; i++)
            count[(arr[i] / exp) % 10]++;

        for (int i = 1; i < 10; i++)
            count[i] += count[i - 1];

        for (int i = n - 1; i >= 0; i--) {
            output[count[(arr[i] / exp) % 10] - 1] = arr[i];
            count[(arr[i] / exp) % 10]--;
        }

        for (int i = 0; i < n; i++)
            arr[i] = output[i];
    }
}

基数排序的时间复杂度是 O(d * (n + k)),其中 d 是位数,k 是基数(这里是10)。当数据范围不大时,它比 O(n log n) 的排序还要快。

注意:基数排序只适用于整数或可以拆分成独立位的数据。我在项目中遇到过有人想用它排浮点数,结果搞得很复杂。另外,它需要额外的桶空间,数据量大时内存开销不小。

五、四种排序对比

排序算法 时间复杂度 空间复杂度 稳定性 适用场景
简单选择排序 O(n²) O(1) 不稳定 小数据量,教学演示
堆排序 O(n log n) O(1) 不稳定 嵌入式系统,内存受限
归并排序 O(n log n) O(n) 稳定 大数据量,需要稳定性
基数排序 O(d*(n+k)) O(n+k) 稳定 整数排序,范围小

六、知识体系图

下面这张图帮你理清今天讲的内容之间的关系:

排序进阶知识体系 简单选择排序 堆排序 归并排序 基数排序 比较排序 比较排序 比较排序 非比较排序 每次选最小 堆顶取最值 分治合并 按位分桶 复杂度对比 O(n²) O(n log n) O(n log n) O(d*(n+k)) 空间: O(1) 空间: O(1) 空间: O(n) 空间: O(n+k) 不稳定 不稳定 稳定 稳定

好了,今天的内容就到这里。这四种排序各有各的脾气,选哪个得看你的数据规模、内存限制、是否需要稳定排序。我个人建议:嵌入式开发优先考虑堆排序,大数据量且需要稳定用归并排序,整数排序且范围不大可以试试基数排序。至于简单选择排序——嗯,写写作业还行,生产环境慎用。