【数据结构与算法】第35篇:归并排序与基数排序

目录

一、归并排序

[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. 解决:递归地对两半进行归并排序

  3. 合并:将两个有序子数组合并成一个有序数组

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 算法思想

基数排序不比较元素大小,而是按位分配:

  1. 将整数按某一位(个位、十位...)分配到0-9的桶中

  2. 按桶顺序收集

  3. 重复处理下一位

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),但常数较大

下一篇我们讲排序大总结。


十、思考题

  1. 归并排序的空间复杂度能否优化到 O(1)?如何实现?

  2. 为什么归并排序适合外部排序(磁盘文件排序)?

  3. 基数排序中,如果数据包含负数,应该如何处理?

  4. 对于字符串数组,LSD和MSD哪个更合适?为什么?

欢迎在评论区讨论你的答案。

相关推荐
徐小夕11 小时前
花了一周,3亿tokens,我开源了一款 Word 文档智能审查平台,文稿自动质检+可视化分析,告别低效人工审核
前端·算法·github
可编程芯片开发12 小时前
UPFC统一潮流控制器的simulink建模与仿真
算法
小小仙子12 小时前
矢量网络分析仪如何测试S参数的?
人工智能·算法·机器学习
code bean13 小时前
【C#】 `Channel<T>` 深度解析:生产者-消费者模式的现代解法
数据结构·c#
啊啊啊迈 旋棍14 小时前
【译】TypeScript 7 测试版已在 Visual Studio 2026 18.6 Insiders 3 中默认启用
ubuntu·typescript·visual studio
中微极客14 小时前
降维算法75倍加速:从PCA到稀疏字典学习的工程实践
人工智能·学习·算法
storyseek14 小时前
前缀和实现Kogge-Stone算法
数据结构·算法
ziguo112214 小时前
深入浅出 C/C++ 数据类型:从入门到踩坑
linux·c语言·c++·windows·visual studio
元Y亨H15 小时前
开发者必须掌握的十大核心算法
算法