【数据结构与算法】第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哪个更合适?为什么?

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

相关推荐
留白_9 分钟前
【决策树】泰坦尼克号生存预测
算法·决策树·机器学习
hqzing19 分钟前
鸿蒙 PC 底层开发技术详解(七):二进制自签名算法的实现
算法·华为·harmonyos
AI科技星28 分钟前
超复数全域经济周期场与信息谱场——金融与密码学底层理论重构《0·1·∞三元一体全域超复数统一场论》系列全集(六一字不漏完整合订终版)
人工智能·算法·金融·密码学·拓扑学·乖乖数学·全域数学
AI科技星1 小时前
《01无穷全域信息场论:算子G与宇宙本体高维完备公理大典》
人工智能·python·算法·金融·乖乖数学·全域数学
Tim_101 小时前
【C++】013、空类占多少字节?
算法
Ricky_Theseus1 小时前
并查集:连通性问题
算法
Tairitsu_H2 小时前
[LC优选算法#18] 前缀和 | 除⾃⾝以外数组的乘积 | 和为K的⼦数组 | 和可被K整除的⼦数组
算法·前缀和·哈希算法
凯瑟琳.奥古斯特2 小时前
力扣1008:前序重建BST
开发语言·c++·算法·leetcode·职场和发展
qq_383189622 小时前
07-运放运用电路-程控增益放大器
算法
aqiu1111113 小时前
【算法日记 13】LeetCode 49. 字母异位词分组:哈希表的进阶“降维打击”
算法·leetcode·散列表