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

排序,是数据结构与算法里最基础、也最常考的内容。说实话,我工作十几年了,面试新人时还是喜欢从排序聊起。为什么?因为排序算法能看出一个人的基本功——对循环的理解、对复杂度的敏感、对代码细节的把控。

这一章我们聊四个经典排序:冒泡、选择、插入、希尔。它们都属于比较排序,时间复杂度从 O(n²) 到 O(n log n) 不等。我会结合自己的踩坑经历,把每个算法的核心逻辑、代码实现、以及实际应用场景讲清楚。

本章核心知识点

  • 冒泡排序:相邻元素两两比较,大的往后冒
  • 选择排序:每次选最小的,放到已排序末尾
  • 插入排序:像打扑克牌一样,把新牌插入到合适位置
  • 希尔排序:插入排序的升级版,先分组再排序
排序算法(一)知识体系 比较排序 冒泡排序 O(n²) 稳定 相邻比较,大数上浮 选择排序 O(n²) 不稳定 每轮选最小,交换 插入排序 O(n²) 稳定 像打牌,插入合适位置 希尔排序 O(n log n)~O(n²) 插入排序的优化版

1. 冒泡排序

冒泡排序的思路很简单:从头到尾,两两比较相邻元素,如果前一个比后一个大,就交换。这样一轮下来,最大的元素就「冒」到了最后。重复这个过程,直到所有元素有序。

我记得刚学编程时,老师用一杯水里的气泡来比喻——大的气泡先浮上去。嗯,这个比喻挺形象的。

// 冒泡排序 - 标准实现
void bubble_sort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        // 每轮比较次数递减
        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;
            }
        }
    }
}

优化小技巧

如果某一轮没有发生任何交换,说明数组已经有序了。可以加一个标志位提前退出。我在项目中处理接近有序的数据时,这个优化能省不少时间。

// 冒泡排序 - 优化版(带提前退出)
void bubble_sort_optimized(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;  // 没有交换,提前结束
    }
}

时间复杂度:最好 O(n),最坏 O(n²),平均 O(n²)。
空间复杂度:O(1),原地排序。
稳定性:稳定。相等的元素不会交换位置。

2. 选择排序

选择排序的思路更直接:每一轮找到未排序部分的最小元素,把它放到已排序部分的末尾。说白了,就是「矮子里拔将军」,每次挑一个最小的放前面。

我曾经在一个嵌入式项目里用过选择排序——数据量很小(不到100个),而且对稳定性没要求。选择排序的交换次数少,在某些场景下反而比冒泡快。

// 选择排序
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;
        }
    }
}

注意:选择排序是不稳定的

举个例子:数组 [5a, 5b, 3],第一轮会把 3 和 5a 交换,结果变成 [3, 5b, 5a]。两个 5 的相对顺序变了。如果你需要稳定排序,别用选择排序。

时间复杂度:最好 O(n²),最坏 O(n²),平均 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)
  • 数据量小(<50)时,比很多 O(n log n) 算法都快
  • 稳定排序,适合对稳定性有要求的场景

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

4. 希尔排序

希尔排序是插入排序的改进版。它解决了插入排序的一个痛点:每次只能移动一个位置。如果最小的元素在最后面,插入排序要移动 n-1 次才能把它放到最前面。

希尔排序的做法是:先分组,对每组进行插入排序。然后缩小分组间隔,再排序。直到间隔为 1,就是一次完整的插入排序。

我记得第一次看希尔排序时,觉得这思路挺巧的——先让数组「基本有序」,最后再用插入排序收尾。这样既利用了插入排序在基本有序时的高效,又避免了大量移动。

// 希尔排序
void shell_sort(int arr[], int n) {
    // 初始间隔为 n/2,每次减半
    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;
        }
    }
}

关于间隔序列

上面用的是最简单的间隔序列(每次除以2)。实际工程中,有更好的选择,比如 Hibbard 序列(1, 3, 7, 15...)或 Sedgewick 序列。不同的间隔序列会影响时间复杂度。我一般用 n/2 递减,简单够用。

时间复杂度:取决于间隔序列。最坏 O(n²),平均约 O(n log n) ~ O(n^1.3)。
空间复杂度:O(1)。
稳定性:不稳定。分组排序会破坏相对顺序。

四种排序对比

算法 最好时间 最坏时间 平均时间 空间 稳定 适用场景
冒泡排序 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) O(1) 不稳定 中等数据量

避坑指南:我曾经犯过的错

我曾经在嵌入式设备上处理传感器数据排序,数据量大概 200 个左右。一开始用了冒泡排序,结果发现每次排序要跑好几毫秒,设备响应变慢。后来换成希尔排序,时间降到了不到 1 毫秒。所以啊,别小看排序算法的选择,在资源受限的环境里,这点差距可能就是能不能用的区别。

好了,这四种排序就聊到这里。它们虽然简单,但理解透了,后面学快速排序、归并排序时就会轻松很多。记住:没有最好的排序,只有最合适的排序。根据数据量、有序程度、稳定性要求来选,才是工程师该有的思路。

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