目录
[1.1 算法思想](#1.1 算法思想)
[1.2 图解示例](#1.2 图解示例)
[1.3 合并过程详解](#1.3 合并过程详解)
[1.4 代码实现](#1.4 代码实现)
[2.1 时间复杂度](#2.1 时间复杂度)
[2.2 空间复杂度](#2.2 空间复杂度)
[2.3 稳定性](#2.3 稳定性)
[4.1 算法思想](#4.1 算法思想)
[4.2 LSD(最低位优先)](#4.2 LSD(最低位优先))
[4.3 LSD代码实现](#4.3 LSD代码实现)
[5.1 MSD(最高位优先)](#5.1 MSD(最高位优先))
[5.2 LSD vs MSD](#5.2 LSD vs MSD)
[6.1 时间复杂度](#6.1 时间复杂度)
[6.2 空间复杂度](#6.2 空间复杂度)
[6.3 适用场景](#6.3 适用场景)
一、归并排序
1.1 算法思想
归并排序采用分治策略:
-
分解:将数组分成两半
-
解决:递归地对两半进行归并排序
-
合并:将两个有序子数组合并成一个有序数组
1.2 图解示例
text
初始: [5, 2, 4, 6, 1, 3]
分解:
[5,2,4] [6,1,3]
[5,2] [4] [6,1] [3]
[5][2] [6][1]
合并:
[2,5] [4] → [2,4,5]
[1,6] [3] → [1,3,6]
最终合并:[1,2,3,4,5,6]
1.3 合并过程详解
合并两个有序数组 [2,4,5] 和 [1,3,6]:
text
比较2和1 → 取1 → [1]
比较2和3 → 取2 → [1,2]
比较4和3 → 取3 → [1,2,3]
比较4和6 → 取4 → [1,2,3,4]
比较5和6 → 取5 → [1,2,3,4,5]
剩余6 → [1,2,3,4,5,6]
1.4 代码实现
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// 合并两个有序子数组 [left, mid] 和 [mid+1, right]
void merge(int arr[], int left, int mid, int right, int temp[]) {
int i = left; // 左子数组起始
int j = mid + 1; // 右子数组起始
int k = left; // 临时数组索引
// 比较两个子数组,取较小者放入temp
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];
}
}
// 归并排序递归实现
void mergeSort(int arr[], int left, int right, int temp[]) {
if (left >= right) return;
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid, temp);
mergeSort(arr, mid + 1, right, temp);
merge(arr, left, mid, right, temp);
}
// 归并排序入口
void mergeSortWrapper(int arr[], int n) {
int *temp = (int*)malloc(n * sizeof(int));
mergeSort(arr, 0, n - 1, temp);
free(temp);
}
// 打印数组
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {5, 2, 4, 6, 1, 3};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组: ");
printArray(arr, n);
mergeSortWrapper(arr, n);
printf("排序后: ");
printArray(arr, n);
return 0;
}
运行结果:
text
原数组: 5 2 4 6 1 3
排序后: 1 2 3 4 5 6
二、归并排序的复杂度分析
2.1 时间复杂度
归并排序的时间复杂度是稳定的 O(n log n):
text
递推式:T(n) = 2T(n/2) + O(n)
解得:T(n) = O(n log n)
| 情况 | 时间复杂度 |
|---|---|
| 最好 | O(n log n) |
| 最坏 | O(n log n) |
| 平均 | O(n log n) |
2.2 空间复杂度
归并排序需要 O(n) 的额外空间来存储临时数组。
text
每层递归需要 O(n) 的临时空间
递归深度为 O(log n)
但同一时间只使用一份临时数组,所以总空间为 O(n)
2.3 稳定性
归并排序是稳定排序。在合并时,当左右元素相等时,先取左边的,保证了稳定性。
三、归并排序的性能测试
c
void testMergeSortPerformance() {
srand(time(NULL));
int sizes[] = {1000, 10000, 50000, 100000};
int nTests = sizeof(sizes) / sizeof(sizes[0]);
printf("=== 归并排序性能测试 ===\n");
for (int t = 0; t < nTests; t++) {
int n = sizes[t];
int *arr = (int*)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
arr[i] = rand() % 10000;
}
clock_t start = clock();
mergeSortWrapper(arr, n);
clock_t end = clock();
double time = (double)(end - start) / CLOCKS_PER_SEC * 1000;
printf("n=%d: %.2f ms\n", n, time);
free(arr);
}
}
运行结果(示例):
text
=== 归并排序性能测试 ===
n=1000: 0.25 ms
n=10000: 2.18 ms
n=50000: 12.45 ms
n=100000: 26.83 ms
四、基数排序
4.1 算法思想
基数排序不比较元素大小,而是按位分配:
-
将整数按某一位(个位、十位...)分配到0-9的桶中
-
按桶顺序收集
-
重复处理下一位
4.2 LSD(最低位优先)
从个位开始,依次向高位处理。
示例 :[170, 45, 75, 90, 2, 802, 24, 66]
text
按个位分配:
桶0: 170, 90
桶2: 2, 802
桶4: 24
桶5: 45, 75
桶6: 66
收集:170, 90, 2, 802, 24, 45, 75, 66
按十位分配:
桶0: 2, 802
桶2: 24
桶4: 45
桶6: 66
桶7: 170, 75
桶9: 90
收集:2, 802, 24, 45, 66, 170, 75, 90
按百位分配:
桶0: 2, 24, 45, 66, 75, 90
桶1: 170
桶8: 802
收集:2, 24, 45, 66, 75, 90, 170, 802
排序完成!
4.3 LSD代码实现
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 获取数字的第d位(d=0表示个位)
int getDigit(int num, int d) {
for (int i = 0; i < d; i++) {
num /= 10;
}
return num % 10;
}
// LSD基数排序
void radixSortLSD(int arr[], int n) {
if (n <= 0) return;
// 1. 找到最大值,确定位数
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) max = arr[i];
}
int maxDigits = 0;
while (max > 0) {
maxDigits++;
max /= 10;
}
// 2. 分配桶(10个桶,每个桶最多n个元素)
int **buckets = (int**)malloc(10 * sizeof(int*));
int *bucketSizes = (int*)calloc(10, sizeof(int));
for (int i = 0; i < 10; i++) {
buckets[i] = (int*)malloc(n * sizeof(int));
}
// 3. 对每一位进行分配和收集
for (int d = 0; d < maxDigits; d++) {
// 重置桶大小
for (int i = 0; i < 10; i++) {
bucketSizes[i] = 0;
}
// 分配:将元素放入对应桶
for (int i = 0; i < n; i++) {
int digit = getDigit(arr[i], d);
buckets[digit][bucketSizes[digit]++] = arr[i];
}
// 收集:按桶顺序取回元素
int index = 0;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < bucketSizes[i]; j++) {
arr[index++] = buckets[i][j];
}
}
}
// 释放内存
for (int i = 0; i < 10; i++) {
free(buckets[i]);
}
free(buckets);
free(bucketSizes);
}
// 打印数组
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {170, 45, 75, 90, 2, 802, 24, 66};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组: ");
printArray(arr, n);
radixSortLSD(arr, n);
printf("排序后: ");
printArray(arr, n);
return 0;
}
运行结果:
text
原数组: 170 45 75 90 2 802 24 66
排序后: 2 24 45 66 75 90 170 802
五、基数排序的优化与变体
5.1 MSD(最高位优先)
从高位开始分配,适用于字符串排序,可以提前终止。
c
// MSD基数排序(递归,处理范围[lo, hi],当前位d)
void radixSortMSD(int arr[], int lo, int hi, int d, int maxDigits) {
if (lo >= hi || d >= maxDigits) return;
int buckets[10] = {0};
int *temp = (int*)malloc((hi - lo + 1) * sizeof(int));
// 统计每个桶的大小
for (int i = lo; i <= hi; i++) {
int digit = getDigit(arr[i], maxDigits - 1 - d);
buckets[digit]++;
}
// 计算桶的起始位置
int start[10];
start[0] = 0;
for (int i = 1; i < 10; i++) {
start[i] = start[i - 1] + buckets[i - 1];
}
// 分配
for (int i = lo; i <= hi; i++) {
int digit = getDigit(arr[i], maxDigits - 1 - d);
temp[start[digit]++] = arr[i];
}
// 复制回原数组
for (int i = 0; i < hi - lo + 1; i++) {
arr[lo + i] = temp[i];
}
// 递归处理每个桶
int pos = lo;
for (int i = 0; i < 10; i++) {
if (buckets[i] > 0) {
radixSortMSD(arr, pos, pos + buckets[i] - 1, d + 1, maxDigits);
pos += buckets[i];
}
}
free(temp);
}
5.2 LSD vs MSD
| 对比项 | LSD | MSD |
|---|---|---|
| 处理顺序 | 个位→十位→百位 | 百位→十位→个位 |
| 实现方式 | 迭代 | 递归 |
| 空间复杂度 | O(n) | O(n) |
| 适用场景 | 整数、定长字符串 | 变长字符串(可提前终止) |
| 稳定性 | 稳定 | 稳定 |
六、基数排序的复杂度分析
6.1 时间复杂度
-
对 d 位数字,每位需要 O(n) 的分配和收集
-
总时间复杂度:O(d × n)
对于32位整数,d=10(十进制)或 d=32(二进制),即 O(n)
6.2 空间复杂度
需要 O(n + k) 的额外空间,k=10(桶的数量)
6.3 适用场景
| 优点 | 缺点 |
|---|---|
| 时间复杂度 O(n) | 只能用于整数或字符串 |
| 稳定排序 | 需要额外空间 |
| 适合大数据量 | 对负数处理较麻烦 |
七、三种排序算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 是否比较 |
|---|---|---|---|---|
| 归并排序 | O(n log n) | O(n) | 稳定 | 是 |
| 基数排序 | O(d×n) | O(n+k) | 稳定 | 否 |
| 快速排序 | O(n log n) | O(log n) | 不稳定 | 是 |
八、完整性能测试
c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 归并排序(略,见上文)
// 基数排序(略,见上文)
// 快速排序(简单实现)
void quickSort(int arr[], int left, int right) {
if (left >= right) return;
int pivot = arr[left];
int i = left, j = right;
while (i < j) {
while (i < j && arr[j] >= pivot) j--;
arr[i] = arr[j];
while (i < j && arr[i] <= pivot) i++;
arr[j] = arr[i];
}
arr[i] = pivot;
quickSort(arr, left, i - 1);
quickSort(arr, i + 1, right);
}
void testAllSorts() {
srand(time(NULL));
int n = 50000;
int *arr1 = (int*)malloc(n * sizeof(int));
int *arr2 = (int*)malloc(n * sizeof(int));
int *arr3 = (int*)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
int val = rand() % 100000;
arr1[i] = arr2[i] = arr3[i] = val;
}
clock_t start, end;
start = clock();
mergeSortWrapper(arr1, n);
end = clock();
printf("归并排序: %.2f ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000);
start = clock();
quickSort(arr2, 0, n - 1);
end = clock();
printf("快速排序: %.2f ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000);
start = clock();
radixSortLSD(arr3, n);
end = clock();
printf("基数排序: %.2f ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000);
free(arr1);
free(arr2);
free(arr3);
}
int main() {
testAllSorts();
return 0;
}
运行结果(示例):
text
归并排序: 12.45 ms
快速排序: 8.32 ms
基数排序: 6.78 ms
九、小结
这一篇我们学习了归并排序和基数排序:
| 算法 | 核心思想 | 时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 归并排序 | 分治+合并 | O(n log n) | O(n) | 稳定 |
| 基数排序 | 按位分配收集 | O(d×n) | O(n+k) | 稳定 |
归并排序要点:
-
分治策略,先分后合
-
需要 O(n) 额外空间
-
稳定排序,适合外部排序
基数排序要点:
-
非比较排序,按位处理
-
LSD从低位到高位,MSD从高位到低位
-
时间复杂度 O(n),但常数较大
下一篇我们讲排序大总结。
十、思考题
-
归并排序的空间复杂度能否优化到 O(1)?如何实现?
-
为什么归并排序适合外部排序(磁盘文件排序)?
-
基数排序中,如果数据包含负数,应该如何处理?
-
对于字符串数组,LSD和MSD哪个更合适?为什么?
欢迎在评论区讨论你的答案。