排序算法:选择排序、插入排序、快速排序、归并排序的原理与实现

排序,是编程里最基础也最常碰到的操作之一。我做了这么多年嵌入式开发,几乎每个项目都离不开排序——从传感器数据整理到任务调度优先级排列。今天咱们就把四种经典排序算法掰开揉碎讲清楚。

说实话,刚入行那会儿我也觉得排序嘛,能排出来就行。后来在实时系统里踩过坑才明白:选错排序算法,轻则响应变慢,重则系统崩溃。所以这一章,咱们不光要懂原理,更要学会在什么场景下选什么算法。

一、选择排序——最直观的排序

选择排序的思路特别简单:每次从待排序的数据中找出最小的那个,放到最前面。就像你手里有一把扑克牌,一张张挑最小的往桌上摆。

原理

遍历数组,找到最小值的位置,然后跟第一个元素交换。接着从第二个位置开始,重复这个过程。

代码实现

void selection_sort(int arr[], int n) {
    int i, j, min_idx, temp;
    for (i = 0; i < n - 1; i++) {
        min_idx = i;
        for (j = i + 1; j < n; j++) {
            if (arr[j] < arr[min_idx]) {
                min_idx = j;
            }
        }
        if (min_idx != i) {
            temp = arr[i];
            arr[i] = arr[min_idx];
            arr[min_idx] = temp;
        }
    }
}

复杂度分析

指标
最好情况O(n²)
最坏情况O(n²)
平均情况O(n²)
空间复杂度O(1)
稳定性不稳定

我的经验:选择排序虽然慢,但它的交换次数是O(n),比冒泡排序少得多。我在做低功耗设备时,如果数据量很小(比如少于20个),偶尔会用选择排序——因为它的代码量小,不容易出bug。

二、插入排序——像整理扑克牌

插入排序的思路更贴近生活:你打牌的时候,摸到一张新牌,会把它插到手里已经排好序的牌中合适的位置。插入排序就是干这个的。

原理

从第二个元素开始,把它跟前面的元素比较,找到合适的位置插入。前面的元素依次后移。

代码实现

void insertion_sort(int arr[], int n) {
    int i, j, key;
    for (i = 1; i < n; i++) {
        key = arr[i];
        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)
稳定性稳定

关键点:插入排序在数据基本有序时效率极高,接近O(n)。我曾在处理实时日志数据时用过它——因为日志本身按时间戳生成,基本有序,插入排序跑得飞快。

避坑指南:我曾经在嵌入式项目中用插入排序处理一个接近逆序的数组,结果系统直接超时。后来才意识到,数据分布对插入排序影响巨大——逆序时它是最慢的排序之一。

三、快速排序——分而治之的典范

快速排序是实际应用中最常用的排序算法。C标准库里的qsort就是它。为什么叫「快速」?因为它平均性能确实好。

原理

选一个基准值(pivot),把小于基准的放左边,大于基准的放右边。然后对左右两部分递归执行同样的操作。

代码实现

int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    int j, temp;
    
    for (j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    return i + 1;
}

void quick_sort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quick_sort(arr, low, pi - 1);
        quick_sort(arr, pi + 1, high);
    }
}

复杂度分析

指标
最好情况O(n log n)
最坏情况O(n²)
平均情况O(n log n)
空间复杂度O(log n)
稳定性不稳定

我的建议:快速排序的基准选择很关键。我习惯用「三数取中法」——取第一个、中间、最后一个元素的中位数作为基准。这样能有效避免最坏情况。在STM32上处理传感器数据时,这个方法帮我省了不少时间。

四、归并排序——稳定且高效

归并排序是另一种分治思想的应用。它先把数组拆成单个元素,再两两合并成有序序列。说白了就是「先拆后合」。

原理

把数组从中间分成两半,分别排序,然后合并两个有序数组。递归进行,直到每个子数组只有一个元素。

代码实现

void merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;
    int L[n1], R[n2];
    int i, j, k;
    
    for (i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];
    
    i = 0; j = 0; k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    while (i < n1) {
        arr[k] = L[i];
        i++; k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++; k++;
    }
}

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

复杂度分析

指标
最好情况O(n log n)
最坏情况O(n log n)
平均情况O(n log n)
空间复杂度O(n)
稳定性稳定

注意:归并排序需要额外的O(n)空间。我在做资源受限的嵌入式项目时,如果内存紧张,会优先考虑快速排序而不是归并排序。但如果你需要稳定排序,归并是很好的选择。

五、四种排序算法对比

算法平均时间最坏时间空间稳定适用场景
选择排序O(n²)O(n²)O(1)小数据量,交换次数少
插入排序O(n²)O(n²)O(1)基本有序的数据
快速排序O(n log n)O(n²)O(log n)通用场景,大数据量
归并排序O(n log n)O(n log n)O(n)需要稳定排序,内存充足

六、知识体系总览

下面这张图帮你理清这四种排序算法的核心逻辑和关系:

排序算法知识体系 选择排序 插入排序 快速排序 归并排序 每次选最小 放到前面 逐个插入 到有序序列 分治+基准 递归划分 先拆后合 两两合并 简单排序 O(n²) 简单排序 O(n²) 高效排序 O(n log n) 高效排序 O(n log n) 如何选择? 数据量小(<50)→ 插入排序 数据量大且内存充足 → 快速排序或归并排序

七、实际项目中的选择建议

说了这么多理论,咱们聊聊实际怎么选。我个人总结了几条经验:

  • 数据量小于50:直接用插入排序。代码简单,而且在小数据量下性能不差。
  • 数据基本有序:插入排序是王者。我在处理日志文件时经常用。
  • 数据量大且内存充足:快速排序是首选。C标准库都选它,你想想看为什么?
  • 需要稳定排序:归并排序。比如按时间戳排序后还要保持原始顺序。
  • 内存极度受限:快速排序(原地排序)或插入排序。

最后说一句:排序算法没有银弹。理解每种算法的特点和适用场景,比死记硬背代码重要得多。我在项目中见过太多人盲目用快速排序,结果在小数据量下反而比插入排序慢——因为递归调用的开销摆在那里。

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