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

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

相关推荐
Elsa️7468 分钟前
算法一周刷题总结
c++·算法
小小程序猴126 分钟前
深圳乘路资讯AI培训怎么样?课程靠谱吗?——一套课程可信度评估模型与实证分析
人工智能·算法
云上先途30 分钟前
标签化服务适合哪些人?常见适用场景一次讲清
大数据·人工智能·算法·音视频
彧azz1 小时前
DFS与BFS:图遍历的两大核心算法
数据结构·学习·算法·深度优先·广度优先
宣宣猪的小花园.1 小时前
【机器学习】损失函数与梯度下降:机器如何通过“犯错”不断变好
人工智能·算法·机器学习
青山木2 小时前
Hot 100 --- 最长递增子序列
java·数据结构·算法·leetcode·动态规划
土司大王2 小时前
LeetCode hot100——33.搜索旋转排序数组:Java 二分模板与 O(log n) 实现
数据结构·算法·leetcode
不穿鞋的懒羊羊2 小时前
STL容器
算法
huang5791473 小时前
多线程环境下的共享数据结构安全问题分析4
算法
地平线开发者3 小时前
模型部署|征程6 量化路线怎么选:PTQ 还是 QAT?
神经网络·算法·机器学习