排序算法是软件设计师考试数据结构与算法 部分的核心内容,每年必考。下面我将按照考试大纲要求,对所有排序算法进行系统讲解。
一、排序算法概览与分类
1.1 按算法思想分类
| 类别 | 算法 | 时间复杂度(平均) | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 插入类 | 直接插入排序 | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3) | O(1) | 不稳定 | |
| 交换类 | 冒泡排序 | O(n²) | O(1) | 稳定 |
| 快速排序 | O(n log n) | O(log n) | 不稳定 | |
| 选择类 | 简单选择排序 | O(n²) | O(1) | 不稳定 |
| 堆排序 | O(n log n) | O(1) | 不稳定 | |
| 归并类 | 归并排序 | O(n log n) | O(n) | 稳定 |
| 基数类 | 基数排序 | O(d(n+r)) | O(n+r) | 稳定 |
1.2 必须掌握的核心考点
- 算法思想:手写伪代码或填空
- 时间复杂度分析:最好、最坏、平均
- 稳定性判断:相等元素相对位置是否改变
- 适用场景:数据规模、初始状态
- 优化方法:针对特定情况的改进
二、插入类排序(逐个插入有序序列)
2.1 直接插入排序
算法思想
将待排序序列分为已排序 和未排序两部分,每次从未排序部分取出第一个元素,在已排序部分找到合适位置插入。
执行过程示例
初始: [49, 38, 65, 97, 76, 13, 27]
第1轮:38插入[49] → [38, 49, 65, 97, 76, 13, 27]
第2轮:65插入[38,49] → [38, 49, 65, 97, 76, 13, 27]
第3轮:97插入[38,49,65] → [38, 49, 65, 97, 76, 13, 27]
第4轮:76插入[38,49,65,97] → [38, 49, 65, 76, 97, 13, 27]
第5轮:13插入[38,49,65,76,97] → [13, 38, 49, 65, 76, 97, 27]
第6轮:27插入[13,38,49,65,76,97] → [13, 27, 38, 49, 65, 76, 97]
代码实现(C语言)
c
void InsertSort(int arr[], int n) {
int i, j, temp;
for (i = 1; i < n; i++) { // 默认arr[0]已排序
temp = arr[i]; // 待插入元素
j = i - 1;
while (j >= 0 && arr[j] > temp) { // 找插入位置
arr[j + 1] = arr[j]; // 元素后移
j--;
}
arr[j + 1] = temp; // 插入
}
}
复杂度分析
- 最好情况(已有序):O(n),只比较n-1次,不移动
- 最坏情况(逆序):O(n²),比较+移动约n²/2次
- 平均情况:O(n²)
- 空间复杂度:O(1),只需一个临时变量
考点解析
- 稳定性:稳定。因为碰到相等元素时,插入位置在相等元素后面
- 适用场景 :
- 数据量小(n < 1000)
- 数据基本有序(效率接近O(n))
- 要求稳定性
- 考试常见题 :
- 给定序列,写出每轮排序结果
- 计算比较次数和移动次数
- 与选择排序比较(插入排序对有序数据效率极高)
2.2 希尔排序(缩小增量排序)
算法思想
是直接插入排序的改进版。通过分组 插入排序,让元素快速移动到大致正确的位置,最后整体进行一次插入排序。核心是增量序列的选取。
执行过程示例(增量序列:5, 3, 1)
初始: [49, 38, 65, 97, 76, 13, 27, 49, 55, 04]
第一趟(dk=5):分成5组
组1: [49, 13] → [13, 49]
组2: [38, 27] → [27, 38]
组3: [65, 49] → [49, 65]
组4: [97, 55] → [55, 97]
组5: [76, 04] → [04, 76]
结果:[13, 27, 49, 55, 04, 49, 38, 65, 97, 76]
第二趟(dk=3):分成3组
组1: [13, 55, 38, 76] → 插入排序 → [13, 38, 55, 76]
组2: [27, 04, 65] → [04, 27, 65]
组3: [49, 49, 97] → [49, 49, 97]
结果:[13, 04, 49, 38, 27, 49, 55, 65, 97, 76]
第三趟(dk=1):直接插入排序
结果:[04, 13, 27, 38, 49, 49, 55, 65, 76, 97]
代码实现
c
void ShellSort(int arr[], int n) {
int i, j, temp, dk;
// 增量序列:n/2, n/4, ..., 1
for (dk = n/2; dk >= 1; dk = dk/2) {
for (i = dk; i < n; i++) { // 从每个组的第二个元素开始
temp = arr[i];
j = i - dk;
while (j >= 0 && arr[j] > temp) {
arr[j + dk] = arr[j];
j -= dk;
}
arr[j + dk] = temp;
}
}
}
复杂度分析
- 时间复杂度 :与增量序列选择有关
- Shell原始增量(n/2, n/4, ..., 1):O(n²)
- Hibbard增量(1, 3, 7, ..., 2k-1):O(n(3/2))
- Sedgewick增量:O(n^(4/3))
- 空间复杂度:O(1)
- 稳定性 :不稳定(分组插入可能导致相等元素交换顺序)
考点解析
- 核心思想:利用插入排序在数据基本有序时效率高的特点
- 增量序列特点 :
- 最后一个增量必须为1
- 增量不宜互为倍数(最好互质)
- 考试重点 :
- 给定初始序列和增量序列,写出每趟结果
- 判断稳定性(易错点:希尔排序不稳定)
- 与直接插入排序比较(希尔排序更快,但不稳定)
三、交换类排序(两两比较交换)
3.1 冒泡排序
算法思想
重复遍历序列,每次比较相邻两个元素,顺序错误就交换。每趟遍历会将当前未排序部分的最大(或最小)元素"浮"到最后。
执行过程示例
初始: [49, 38, 65, 97, 76, 13, 27]
第1趟:38,49,65,76,13,27,[97] ← 97归位
比较:49-38交换 38-65不换 65-97不换 97-76交换 97-13交换 97-27交换
第2趟:38,49,65,13,27,[76,97] ← 76归位
第3趟:38,49,13,27,[65,76,97]
第4趟:38,13,27,[49,65,76,97]
第5趟:13,27,[38,49,65,76,97]
第6趟:13,[27,38,49,65,76,97]
优化版代码
c
void BubbleSort(int arr[], int n) {
int i, j, temp;
int swapped; // 优化标志
for (i = 0; i < n-1; i++) {
swapped = 0;
for (j = 0; j < n-1-i; j++) {
if (arr[j] > arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = 1;
}
}
if (swapped == 0) break; // 没有交换,已有序
}
}
复杂度分析
- 最好情况(已有序):O(n),优化后只需一趟
- 最坏情况(逆序):O(n²)
- 平均情况:O(n²)
- 空间复杂度:O(1)
- 稳定性 :稳定(相等元素不交换)
考点解析
- 优化技巧 :
- 设置swapped标志,提前结束
- 记录最后一次交换位置,减少比较次数
- 适用场景:数据量小,或基本有序(教学常用)
- 考试形式 :
- 手工模拟排序过程
- 计算比较次数和交换次数
- 对比其他简单排序
3.2 快速排序 ⭐⭐⭐(重中之重)
算法思想
分治法 的典型应用。选择一个基准元素(pivot),通过一趟排序将序列分为两部分:左边都≤pivot,右边都≥pivot。然后递归地对左右两部分进行同样操作。
核心操作:Partition(划分)
c
int Partition(int arr[], int low, int high) {
int pivot = arr[low]; // 选择第一个元素为基准
int i = low, j = high;
while (i < j) {
// 从右向左找第一个小于pivot的元素
while (i < j && arr[j] >= pivot) j--;
if (i < j) arr[i++] = arr[j];
// 从左向右找第一个大于pivot的元素
while (i < j && arr[i] <= pivot) i++;
if (i < j) arr[j--] = arr[i];
}
arr[i] = pivot; // 基准归位
return i; // 返回基准位置
}
完整代码
c
void QuickSort(int arr[], int low, int high) {
if (low < high) {
int pivotPos = Partition(arr, low, high);
QuickSort(arr, low, pivotPos - 1); // 左半部分递归
QuickSort(arr, pivotPos + 1, high); // 右半部分递归
}
}
执行过程示例
初始: [49, 38, 65, 97, 76, 13, 27]
选择49为基准:
第一次划分:左[27,38,13] 49 右[76,97,65]
递归左:[27,38,13] 选27为基准 → [13] 27 [38] → 归位
递归右:[76,97,65] 选76为基准 → [65] 76 [97] → 归位
最终:[13,27,38,49,65,76,97]
复杂度分析
- 最好情况:每次划分都均匀,T(n)=2T(n/2)+O(n) → O(n log n)
- 最坏情况:序列已有序或逆序,每次划分极不均匀 → O(n²)
- 平均情况:O(n log n)(数学期望)
- 空间复杂度:O(log n)(递归栈深度)
- 稳定性 :不稳定(交换时可能改变相等元素顺序)
优化方法(考试常见)
- 基准选择优化 :
- 随机选择基准
- 三数取中(左、中、右三个位置的中位数)
- 小数组优化:当子数组长度小于阈值(如10),改用插入排序
- 尾递归优化:减少递归深度
考点解析(高频)
- 算法思想:分治 + 划分
- 手工模拟:给定序列,写出每趟划分结果
- 时间复杂度分析:理解最好、最坏、平均情况
- 空间复杂度:递归栈空间(易错点)
- 与归并排序比较 :
- 快排不均匀时可能退化为O(n²),归并始终O(n log n)
- 快排空间O(log n)更好,归并需要O(n)额外空间
- 快排不稳定,归并稳定
- 快排实际运行速度通常更快(常数小)
四、选择类排序(选最小/最大)
4.1 简单选择排序
算法思想
每趟在未排序部分选择最小(或最大)元素,放到已排序部分的末尾。
代码实现
c
void SelectSort(int arr[], int n) {
int i, j, minIdx, temp;
for (i = 0; i < n-1; i++) {
minIdx = i;
for (j = i+1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
}
}
}
执行过程示例
初始: [49, 38, 65, 97, 76, 13, 27]
第1趟:找最小值13,与49交换 → [13, 38, 65, 97, 76, 49, 27]
第2趟:找次小值27,与38交换 → [13, 27, 65, 97, 76, 49, 38]
第3趟:找38,与65交换 → [13, 27, 38, 97, 76, 49, 65]
第4趟:找49,与97交换 → [13, 27, 38, 49, 76, 97, 65]
第5趟:找65,与76交换 → [13, 27, 38, 49, 65, 97, 76]
第6趟:找76,与97交换 → [13, 27, 38, 49, 65, 76, 97]
复杂度分析
- 时间复杂度:始终 O(n²),比较次数固定为 n(n-1)/2
- 空间复杂度:O(1)
- 稳定性 :不稳定(示例中的38和65交换破坏了稳定性)
考点解析
- 与冒泡排序比较 :
- 选择排序交换次数少(最多n-1次),但比较次数固定
- 冒泡排序可提前结束,选择排序不能
- 选择排序不稳定,冒泡稳定
- 特点:实现简单,不依赖初始状态,适合n较小且交换代价高的场景
- 考试易错:误以为选择排序稳定(实际不稳定)
4.2 堆排序 ⭐⭐⭐(数据结构重点)
算法思想
利用堆这种完全二叉树结构进行排序。先构建大顶堆(父节点≥子节点),然后每次将堆顶(最大值)与最后一个元素交换,再调整剩余元素为堆。
核心操作:调整堆(建堆/下沉)
c
// 将以k为根的子树调整为堆,heapSize为当前堆大小
void HeapAdjust(int arr[], int k, int heapSize) {
int temp = arr[k];
int child = 2 * k; // 左孩子
while (child <= heapSize) {
// 选出较大的孩子
if (child < heapSize && arr[child+1] > arr[child]) {
child++;
}
// 如果父节点≥较大孩子,调整结束
if (temp >= arr[child]) break;
arr[child/2] = arr[child]; // 孩子上移
child = 2 * child; // 继续向下调整
}
arr[child/2] = temp;
}
完整代码
c
void HeapSort(int arr[], int n) {
int i, temp;
// 1. 构建大顶堆(从最后一个非叶子节点开始)
for (i = n/2; i >= 1; i--) {
HeapAdjust(arr, i, n);
}
// 2. 反复交换堆顶和末尾,并调整
for (i = n; i > 1; i--) {
// 交换堆顶和堆尾
temp = arr[1];
arr[1] = arr[i];
arr[i] = temp;
// 调整剩余元素为堆
HeapAdjust(arr, 1, i-1);
}
}
执行过程示例
初始数组(索引1开始):[49, 38, 65, 97, 76, 13, 27]
第一步:建堆(大顶堆)
原始数组结构(完全二叉树):
49(1)
/ \
38(2) 65(3)
/ \ /
97(4) 76(5) 13(6) 27(7) ← 索引
从i=3开始调整:
i=3: 65与子节点13,27比较,无需交换
i=2: 38与子节点97,76比较,38与97交换 → 数组:49,97,65,38,76,13,27
38继续与子节点比较,交换后位置4→ 数组:49,97,65,38,76,13,27
i=1: 49与子节点97,65比较,49与97交换 → 数组:97,49,65,38,76,13,27
49继续与子节点38,76比较,49与76交换 → 数组:97,76,65,38,49,13,27
最终大顶堆:97,76,65,38,49,13,27
第二步:排序
交换97和27 → [27,76,65,38,49,13,97] 调整堆 → [76,49,65,38,27,13,97]
交换76和13 → [13,49,65,38,27,76,97] 调整堆 → [65,49,13,38,27,76,97]
交换65和27 → [27,49,13,38,65,76,97] 调整堆 → [49,38,13,27,65,76,97]
... 直到有序
复杂度分析
- 时间复杂度 :
- 建堆:O(n)(从n/2向下调整)
- 排序:n-1次调整,每次O(log n) → O(n log n)
- 总复杂度:O(n log n)
- 空间复杂度:O(1)(原地排序)
- 稳定性 :不稳定(交换堆顶和堆尾可能乱序)
考点解析(高频)
- 建堆过程 :
- 从最后一个非叶子节点(n/2)开始调整
- 时间复杂度O(n)要理解(不是O(n log n))
- 调整堆过程 :
- 下沉操作,每次比较父子节点
- 时间复杂度O(log n)
- 应用场景 :
- 不需要全部有序,只要Top K问题(如找最大的10个数)
- 优先队列的实现
- 考试常见题 :
- 给定序列,写出建堆后的数组
- 写出每次交换和调整后的结果
- 计算堆排序的比较次数
五、归并类排序
5.1 归并排序 ⭐⭐(二路归并)
算法思想
采用分治法,将序列递归地分成两半,分别排序,然后将两个有序子序列合并成一个。
核心操作:合并两个有序序列
c
void Merge(int arr[], int temp[], int left, int mid, int right) {
int i = left; // 左子序列起点
int j = mid + 1; // 右子序列起点
int k = left; // 临时数组索引
// 合并两个有序序列
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 处理剩余元素
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
// 复制回原数组
for (i = left; i <= right; i++) {
arr[i] = temp[i];
}
}
递归版本
c
void MergeSort(int arr[], int temp[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
MergeSort(arr, temp, left, mid); // 左半排序
MergeSort(arr, temp, mid + 1, right); // 右半排序
Merge(arr, temp, left, mid, right); // 合并
}
}
非递归(迭代)版本(考试常见)
c
void MergeSortIter(int arr[], int n) {
int* temp = (int*)malloc(n * sizeof(int));
int len = 1; // 子序列长度
while (len < n) {
int i = 0;
while (i + len < n) {
int left = i;
int mid = i + len - 1;
int right = (i + 2*len - 1 < n-1) ? i + 2*len - 1 : n - 1;
Merge(arr, temp, left, mid, right);
i += 2 * len;
}
len *= 2;
}
free(temp);
}
执行过程示例
初始: [49, 38, 65, 97, 76, 13, 27]
分解过程:
[49,38,65,97,76,13,27]
/ \
[49,38,65,97] [76,13,27]
/ \ / \
[49,38] [65,97] [76,13] [27]
/ \ / \ / \ |
[49][38] [65][97] [76][13] [27]
合并过程(自底向上):
[38,49] [65,97] [13,76] [27]
\ / \ /
[38,49,65,97] [13,27,76]
\ /
[13,27,38,49,65,76,97]
复杂度分析
- 时间复杂度:始终 O(n log n)(划分log n层,每层合并O(n))
- 空间复杂度:O(n)(需要临时数组)
- 稳定性 :稳定(合并时相等元素先取左边)
考点解析
- 特点 :
- 稳定且时间复杂度稳定(最好最坏都是O(n log n))
- 空间复杂度较高(缺点)
- 适合外部排序(大文件排序)
- 与快速排序比较 :
- 归并稳定,快排不稳定
- 归并空间O(n) > 快排O(log n)
- 归并始终O(n log n),快排可能O(n²)
- 实际快排更快(常数小,缓存友好)
- 考试重点 :
- 手工模拟归并过程
- 理解递归和迭代两种实现
- 空间复杂度计算(易错点)
- 外部排序应用:多路归并排序
六、基数排序
6.1 基数排序(多关键字排序)
算法思想
不比较元素大小,而是通过分配和收集进行排序。按位(个位、十位、百位...)进行多次排序。
代码实现(基于计数排序的思想)
c
// 获取最大数的位数
int GetMaxBit(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) max = arr[i];
}
int bits = 0;
while (max > 0) {
bits++;
max /= 10;
}
return bits;
}
// 基数排序
void RadixSort(int arr[], int n) {
int* temp = (int*)malloc(n * sizeof(int));
int* count = (int*)malloc(10 * sizeof(int)); // 0-9
int maxBit = GetMaxBit(arr, n);
int radix = 1; // 当前位数(1,10,100...)
for (int bit = 1; bit <= maxBit; bit++) {
// 初始化计数器
for (int i = 0; i < 10; i++) count[i] = 0;
// 统计当前位的数字出现次数
for (int i = 0; i < n; i++) {
int digit = (arr[i] / radix) % 10;
count[digit]++;
}
// 计算累积计数(确定每个数字的最终位置)
for (int i = 1; i < 10; i++) {
count[i] += count[i-1];
}
// 从后往前分配(保证稳定性)
for (int i = n-1; i >= 0; i--) {
int digit = (arr[i] / radix) % 10;
temp[count[digit] - 1] = arr[i];
count[digit]--;
}
// 复制回原数组
for (int i = 0; i < n; i++) {
arr[i] = temp[i];
}
radix *= 10;
}
free(temp);
free(count);
}
执行过程示例
初始: [49, 38, 65, 97, 76, 13, 27]
第一趟(按个位):
个位:9,8,5,7,6,3,7
分配:桶0-9
桶3:[13] 桶5:[65] 桶6:[76] 桶7:[97,27] 桶8:[38] 桶9:[49]
收集:13,65,76,97,27,38,49
第二趟(按十位):
十位:1,6,7,9,2,3,4
分配:桶1:[13] 桶2:[27] 桶3:[38] 桶4:[49] 桶6:[65] 桶7:[76] 桶9:[97]
收集:13,27,38,49,65,76,97
结果有序!
复杂度分析
- 时间复杂度 :O(d(n+r))
- d:最大位数
- n:元素个数
- r:基数(十进制r=10)
- 空间复杂度:O(n+r)
- 稳定性 :稳定
考点解析
- 适用场景 :
- 整数排序(可推广到字符串、日期)
- 元素位数d较小(如手机号、身份证号)
- 特点 :
- 不比较元素大小,效率受位数影响
- 需要额外空间
- 稳定排序
- 考试重点 :
- LSD(最低位优先)和MSD(最高位优先)
- 手工模拟分配和收集过程
- 时间复杂度公式理解
七、排序算法比较总结表
| 算法 | 最好 | 平均 | 最坏 | 空间 | 稳定性 | 适用场景 | 考试频率 |
|---|---|---|---|---|---|---|---|
| 冒泡 | 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^1.3) | O(n^1.3) | O(n²) | O(1) | ❌ | 中等规模 | ⭐⭐ |
| 快速 | O(n log n) | O(n log n) | O(n²) | O(log n) | ❌ | 大规模通用 | ⭐⭐⭐⭐⭐ |
| 归并 | O(n log n) | O(n log n) | O(n log n) | O(n) | ✅ | 外部排序,稳定性要求高 | ⭐⭐⭐⭐ |
| 堆 | O(n log n) | O(n log n) | O(n log n) | O(1) | ❌ | Top K,优先队列 | ⭐⭐⭐⭐ |
| 基数 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | ✅ | 整数/字符串,d较小 | ⭐⭐ |
八、考试题型与例题解析
题型1:手工模拟排序过程
例题:已知序列 {49, 38, 65, 97, 76, 13, 27},写出快速排序第一趟划分后的结果(基准选第一个)。
解析:
初始:[49, 38, 65, 97, 76, 13, 27]
基准=49,i=0, j=6
① j从右找<49:找到27,arr[0]=27, i=1
② i从左找>49:找到65,arr[6]=65, j=5
③ j继续从右找<49:找到13,arr[1]=13, i=2
④ i从左找>49:找到97,arr[5]=97, j=4
⑤ j继续从右找<49:找到76?76>49,继续找,直到i=j=2
arr[2]=49
结果:[27, 38, 13, 49, 76, 97, 65]
题型2:复杂度分析
例题:对n个元素进行堆排序,在建堆过程中,最多需要多少次比较?
解析 :
建堆时,每个非叶子节点都要进行下沉调整。总比较次数 ≈ 4n(精确分析可得),即O(n)。具体:高度为h的节点下沉最多比较2h次,求和后约为2n。
题型3:稳定性判断
例题 :判断以下哪些排序算法不稳定?(选择题)
A. 冒泡排序 B. 选择排序 C. 插入排序 D. 归并排序
解析:选择排序不稳定,选B。
题型4:应用场景选择
例题 :若需要在O(n log n)时间内完成排序,且要求稳定性,应选择?
A. 快速排序 B. 堆排序 C. 归并排序 D. 希尔排序
解析:归并排序(C)稳定且时间复杂度O(n log n)。快排和堆排不稳定,希尔不稳定且平均O(n^1.3)。
九、备考建议
-
必须手动画出过程:
- 快排的划分过程
- 归并的合并过程
- 堆的建堆和调整过程
-
牢记复杂度表格:
- 最好、最坏、平均时间复杂度
- 空间复杂度(特别注意快排O(log n)递归栈)
-
稳定性口诀:
- 稳定:插(插入)冒(冒泡)归(归并)基(基数)
- 口诀:"饽饽酥鸡 "(谐音"剥剥酥鸡"或"伯伯舒基")
- 饽(冒泡)
- 饽(归并,取"并"谐音)
- 酥(插入,英文Sort,或取"插"的韵母)
- 鸡(基数)
-
算法实现:
- 堆排序的HeapAdjust最容易写错
- 快排的Partition有多种写法
- 归并的Merge必须熟记
-
典型考题:
- 每年下午题常考算法填空(堆排序、快速排序)
- 上午题必考复杂度、稳定性选择题
希望这份详尽的排序算法解析能帮助您顺利通过软考!加油!