主题
第七章 排序算法
📖 ⏱️ 预计阅读时长 1 分钟 👑 会员专属
1. 为什么排序算法值得单独成章
排序是数据处理中最基础、最常见的操作之一。所谓排序,就是按照某种关键字规则,把一组原本无序或部分有序的数据,重新排列成升序或降序序列。表面上看,排序只是“把数据排好顺序”,但从算法角度看,它实际上集中体现了算法设计中的许多核心思想,包括比较、交换、分治、递归、选择、局部有序、稳定性与复杂度分析。
在现实应用中,排序无处不在。成绩排名、商品价格展示、搜索结果排序、日志按时间排列、数据库中的 ORDER BY、操作系统调度中的优先级整理,都与排序思想密切相关。正因为排序既重要又典型,所以在数据结构与算法课程中,通常要单独用一个完整章节来系统讲解。
2. 排序的基本概念
2.1 内部排序与外部排序
- 内部排序:待排序记录全部存放在内存中,排序过程主要受 CPU 运算与内存访问影响。
- 外部排序:待排序数据量大到无法一次全部装入内存,需要借助外存分批处理。
本章讨论的重点主要是内部排序。
2.2 稳定性
若序列中存在多个关键字相同的元素,排序后这些相同关键字元素的相对先后次序保持不变,则称该排序算法是稳定的;反之称为不稳定的。
稳定性在“按多个字段分层排序”时尤为重要。例如先按班级排序,再按成绩排序,如果第二次排序算法稳定,则同成绩同学的班级相对次序不会被破坏。
2.3 原地排序
若排序过程中只使用少量辅助空间,而不需要额外开辟与数据规模同级别的存储空间,则常称为原地排序。原地排序通常更节省空间,但不一定在时间上最优。
2.4 排序算法评价指标
评价排序算法通常从以下几个方面进行:
- 时间复杂度。
- 空间复杂度。
- 稳定性。
- 是否原地。
- 对原始数据分布是否敏感。
- 在小规模数据和大规模数据下的实际表现。
排序算法之所以值得深入研究,正是因为这些指标之间往往不能同时最优。一个算法可能时间复杂度好,但需要较多额外空间;另一个算法可能原地完成,但不稳定;还有的算法平均性能优秀,却在极端情况下可能退化。因此,排序学习的关键不是死记复杂度,而是理解各种代价之间的平衡关系。
3. 排序算法总览图
上图说明,排序算法并不是一组彼此孤立的方法,而是可以按思想分为插入类、选择类、交换类、分治类和分配类。理解这一分类后,记忆和比较会更有体系。
4. 冒泡排序
4.1 基本思想
冒泡排序通过反复比较相邻两个元素,并在顺序错误时交换它们,使较大元素像气泡一样逐步“浮”到序列末端。
4.2 过程特点
- 每一趟排序都会把当前未排序部分中的最大元素放到正确位置。
- 若某一趟没有发生交换,则说明整体已经有序,可以提前结束。
4.3 复杂度与性质
- 最好时间复杂度:
O(n)。 - 最坏时间复杂度:
O(n^2)。 - 空间复杂度:
O(1)。 - 稳定性:稳定。
4.4 动图示意

4.5 C 语言示例
c
#include <stdio.h>
void bubble_sort(int a[], int n) {
int i, j, temp, swapped;
for (i = 0; i < n - 1; i++) {
swapped = 0;
for (j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
swapped = 1;
}
}
if (!swapped) {
break;
}
}
}
int main(void) {
int a[] = {5, 2, 9, 1, 3};
int n = sizeof(a) / sizeof(a[0]);
int i;
bubble_sort(a, n);
for (i = 0; i < n; i++) {
printf("%d ", a[i]);
}
return 0;
}4.6 适用评价
冒泡排序思想直观,非常适合教学入门,但效率较低,不适合大规模数据排序。
从原理上说,冒泡排序的低效来自大量局部交换。每次交换只能让元素向正确位置移动一小步,因此当逆序较多时,需要反复多趟扫描才能完成排序。
5. 选择排序
5.1 基本思想
选择排序每一趟从未排序区间中选择最小元素,将其放到当前应在的位置。
5.2 复杂度与性质
- 时间复杂度:无论最好还是最坏,通常都为
O(n^2)。 - 空间复杂度:
O(1)。 - 稳定性:通常不稳定。
5.3 动图示意

5.4 特点分析
选择排序的交换次数相对较少,但比较次数仍然很多,因此总体效率并不高。它的教学意义大于工程意义。
它之所以通常不稳定,是因为“选出最小元素再交换到前面”这一动作,可能会跨越多个相同关键字元素,从而破坏它们原有的相对次序。
6. 插入排序
6.1 基本思想
插入排序把序列分成“已排序区”和“未排序区”,每次从未排序区取出一个元素,插入到已排序区的合适位置。
这种思想很像整理扑克牌:每次拿起一张新牌,插入到手中已经排好序的位置。
6.2 复杂度与性质
- 最好时间复杂度:
O(n)。 - 最坏时间复杂度:
O(n^2)。 - 空间复杂度:
O(1)。 - 稳定性:稳定。
6.3 动图示意

6.4 C 语言示例
c
#include <stdio.h>
void insertion_sort(int a[], int n) {
int i, j, key;
for (i = 1; i < n; i++) {
key = a[i];
j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}
int main(void) {
int a[] = {8, 4, 6, 2, 7};
int n = sizeof(a) / sizeof(a[0]);
int i;
insertion_sort(a, n);
for (i = 0; i < n; i++) {
printf("%d ", a[i]);
}
return 0;
}6.5 适用评价
插入排序在数据量较小或序列本身“基本有序”时,实际表现往往优于许多看上去更高级的算法。因此,它常被用作复杂排序算法在小区间上的补充策略。
原因在于,当序列已经接近有序时,每次插入所需移动的距离很短,插入排序就不再体现为典型的二次复杂度,而会表现出接近线性的效率。
7. 希尔排序
7.1 基本思想
希尔排序可以看作对插入排序的改进。它先让相距较远的元素进行分组比较和插入,使整体序列逐步趋于有序,最后再进行一次普通插入排序。
7.2 特点分析
- 它利用“先粗调、再精调”的思想改善插入排序在远距离逆序情况下移动过多的问题。
- 时间复杂度与增量序列选择有关,通常优于简单
O(n^2)算法,但不如高效的O(n log n)算法稳定。 - 通常不稳定。
希尔排序在工程中不如快速排序、归并排序和堆排序常见,但在理解“改进插入思想”时很有代表性。
8. 归并排序
8.1 基本思想
归并排序采用典型的分治思想:
- 先把序列不断二分,直到每个子序列长度为 1。
- 再把两个有序子序列逐步合并成更大的有序序列。
8.2 复杂度与性质
- 时间复杂度:
O(n log n)。 - 空间复杂度:
O(n)。 - 稳定性:稳定。
8.3 分治结构图
8.4 动图示意

8.5 C 语言示例
c
#include <stdio.h>
void merge(int a[], int left, int mid, int right, int temp[]) {
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) {
if (a[i] <= a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
while (i <= mid) {
temp[k++] = a[i++];
}
while (j <= right) {
temp[k++] = a[j++];
}
for (i = left; i <= right; i++) {
a[i] = temp[i];
}
}
void merge_sort(int a[], int left, int right, int temp[]) {
int mid;
if (left >= right) {
return;
}
mid = (left + right) / 2;
merge_sort(a, left, mid, temp);
merge_sort(a, mid + 1, right, temp);
merge(a, left, mid, right, temp);
}
int main(void) {
int a[] = {9, 4, 7, 3, 1, 8};
int temp[6];
int i, n = 6;
merge_sort(a, 0, n - 1, temp);
for (i = 0; i < n; i++) {
printf("%d ", a[i]);
}
return 0;
}8.6 适用评价
归并排序时间复杂度稳定,且具有稳定性优势,非常适合需要稳定排序的场景,也适合链表排序和外部排序思想的展开。但它需要额外辅助空间,因此不是典型原地排序。
归并排序之所以稳定,是因为在合并两个有序子序列时,若遇到相等元素,完全可以约定优先取左侧元素,从而保持原有相对次序。这说明稳定性并不是“附带性质”,而是与算法的合并方式直接相关。
9. 快速排序
9.1 基本思想
快速排序同样采用分治思想。它先选取一个基准元素,把序列划分成“比基准小”和“比基准大”的两部分,再对子区间递归排序。
9.2 复杂度与性质
- 平均时间复杂度:
O(n log n)。 - 最坏时间复杂度:
O(n^2)。 - 空间复杂度:递归栈平均
O(log n)。 - 稳定性:通常不稳定。
9.3 动图示意

9.4 C 语言示例
c
#include <stdio.h>
void quick_sort(int a[], int left, int right) {
int i, j, pivot, temp;
if (left >= right) {
return;
}
i = left;
j = right;
pivot = a[left];
while (i < j) {
while (i < j && a[j] >= pivot) {
j--;
}
while (i < j && a[i] <= pivot) {
i++;
}
if (i < j) {
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
a[left] = a[i];
a[i] = pivot;
quick_sort(a, left, i - 1);
quick_sort(a, i + 1, right);
}
int main(void) {
int a[] = {6, 2, 8, 1, 9, 3, 5};
int i, n = sizeof(a) / sizeof(a[0]);
quick_sort(a, 0, n - 1);
for (i = 0; i < n; i++) {
printf("%d ", a[i]);
}
return 0;
}9.5 适用评价
快速排序通常被认为是工程实践中非常高效的通用排序算法之一,因为它平均性能优秀、缓存局部性较好、原地特性较强。但若基准选择不合理,例如每次都选到极端值,就可能退化。
它之所以在实践中往往很快,是因为划分操作通常在原数组上完成,额外空间小,而且递归后的子区间往往还能较好地利用缓存。但它的最坏情况依然提醒我们:平均性能好不代表任何输入都好。
10. 堆排序
10.1 基本思想
堆排序先把序列构造成堆,再反复取出堆顶元素并重建堆,从而得到有序序列。
10.2 复杂度与性质
- 时间复杂度:
O(n log n)。 - 空间复杂度:
O(1)。 - 稳定性:不稳定。
10.3 动图示意

10.4 特点分析
堆排序的优势在于时间复杂度稳定且空间开销小,但由于频繁调整堆结构,其常数因子往往不如快速排序友好,因此在通用排序中常不是默认首选。
从思想上看,堆排序相当于把“反复找出当前最大值或最小值”的过程结构化。若用普通方法反复寻找最值,每次都可能要线性扫描;而堆则把这种需求预先编码在结构中,使得取堆顶和重建都更高效。
11. 计数排序与基数排序
11.1 计数排序
计数排序适用于关键字范围较小的整数序列。它统计每个取值出现次数,再根据计数结果重建有序序列。
其特点是:
- 时间复杂度通常为
O(n + k),其中k是关键字范围。 - 不是基于比较的排序。
- 当关键字范围过大时,空间开销会明显上升。
11.2 基数排序
基数排序按位或按位组对元素进行多轮分配和收集,通常适用于整数或固定长度字符串。
其特点是:
- 不依赖元素之间的直接比较。
- 常与计数排序等稳定排序配合使用。
- 效率与位数长度和分配方式有关。
12. 常见排序算法比较
| 算法 | 最好时间 | 平均时间 | 最坏时间 | 空间复杂度 | 稳定性 | 备注 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 | 入门直观 |
| 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 | 交换次数少 |
| 插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 | 小规模数据常用 |
| 希尔排序 | 依增量而定 | 一般优于 O(n^2) | 依增量而定 | O(1) | 不稳定 | 插入排序改进 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 分治典型 |
| 快速排序 | O(n log n) | O(n log n) | O(n^2) | O(log n) | 不稳定 | 实战常用 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 利用堆结构 |
| 计数排序 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 可稳定 | 关键字范围小 |
| 基数排序 | 与位数有关 | 与位数有关 | 与位数有关 | 较大 | 可稳定 | 非比较排序 |
13. 排序算法的选择思路
13.1 小规模数据
小规模数据时,插入排序往往足够好,且实现简单。
13.2 需要稳定性
若必须保持相同关键字的原有相对顺序,应优先考虑插入排序、归并排序、计数排序等稳定算法。
13.3 追求综合性能
若目标是通用场景下的整体效率,快速排序通常是高频选择;若还要求最坏复杂度稳定,则更应考虑归并排序或堆排序。
13.4 关键字范围有限
若数据是整数,且取值范围不大,则计数排序可能获得明显优势。
13.5 为什么教材里总反复比较“稳定”和“不稳定”
这是因为稳定性不仅影响排序本身,还影响多关键字处理。例如先按次关键字排,再按主关键字排时,若第二次排序稳定,则第一次排序形成的相对顺序不会被破坏。数据库、报表处理和业务数据整理中,这一点非常重要。
14. 本章小结
排序算法既是数据结构课程的重要组成部分,也是算法思想训练的集中体现。冒泡、选择、插入代表了简单基础的排序方法;归并和快速体现了分治思想;堆排序把树形结构用于排序;计数排序和基数排序则说明排序并不一定完全依赖比较。掌握本章后,不应只停留在“记住几个名字和复杂度”的层面,而应能根据稳定性、时间复杂度、空间复杂度和数据特征,判断哪种排序更适合具体问题。
15. 配图与动图来源
- 冒泡排序动图:Wikimedia Commons,
Bubble-sort-example-300px.gif - 选择排序动图:Wikimedia Commons,
AnimazioneSelectionSort.gif - 插入排序动图:Wikimedia Commons,
Insertion_sort_animation.gif - 归并排序动图:Wikimedia Commons,
Merge-sort-example-300px.gif - 快速排序动图:Wikimedia Commons,
Quicksort-example.gif - 堆排序动图:Wikimedia Commons,
Heapsort-example.gif
