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

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

相关推荐
专注API从业者2 小时前
淘宝商品详情 API 与爬虫技术的边界:合法接入与反爬策略的技术博弈
大数据·数据结构·数据库·爬虫
仟人斩2 小时前
Windows 下把 VSCode 加入右键菜单(注册表方案)
windows·vscode·上下文菜单
爱码小白2 小时前
MySQL 单表查询练习题汇总
数据库·python·算法
橘颂TA2 小时前
【笔试】算法的暴力美学——牛客 NC213140 :除2!
c++·算法·结构与算法
汀、人工智能2 小时前
[特殊字符] 第66课:跳跃游戏
数据结构·算法·数据库架构·图论·bfs·跳跃游戏
汀、人工智能2 小时前
[特殊字符] 第70课:加油站
数据结构·算法·数据库架构·图论·bfs·加油站
wsoz2 小时前
Leetcode普通数组-day5、6
c++·算法·leetcode·数组
y = xⁿ2 小时前
【LeetCode】双指针:同向快慢针
算法·leetcode
啊哦呃咦唔鱼3 小时前
LeetCode hot100-105从前序与中序遍历序列构造二叉树
算法