C语言中的排序算法:快速排序、归并排序、堆排序的实现与性能对比

#新星杯·14天创作挑战营·第11期#

        排序算法是计算机科学中不可或缺的一部分,用于将一组数据按照特定顺序排列。在C语言中,常见的排序算法包括快速排序、归并排序和堆排序。每种算法都有其独特的实现方式和性能特点。本文将通过讲故事的方式,深入探讨这三种排序算法的实现与性能对比,并通过实例帮助读者更好地理解和应用这些算法。


一、快速排序:分而治之的高效算法

1. 快速排序的基本原理

快速排序(Quick Sort)是一种分而治之的排序算法。它的基本思想是选择一个基准元素(Pivot),将数组分成两部分:一部分小于基准元素,另一部分大于基准元素。然后递归地对这两部分进行排序,直到整个数组有序。

示例验证:快速排序的实现

#include <stdio.h>  // 包含标准输入输出库

// 交换两个整数的函数
void swap(int* a, int* b) {  // 接收两个整型指针作为参数
    int temp = *a;  // 将指针a指向的值暂存到临时变量temp
    *a = *b;        // 将指针b指向的值赋给指针a指向的内存
    *b = temp;      // 将暂存的temp值赋给指针b指向的内存
}

// 快速排序分区函数,返回基准元素的最终位置
int partition(int arr[], int low, int high) {  // 参数:数组,子数组起始和结束索引
    int pivot = arr[high];  // 选择最后一个元素作为基准元素
    int i = low - 1;        // 初始化较小元素的索引指针(初始为low-1)

    // 遍历数组从low到high-1的所有元素
    for (int j = low; j < high; j++) {  // j从low开始逐个移动
        if (arr[j] <= pivot) {          // 如果当前元素小于等于基准值
            i++;                        // 较小元素索引指针右移
            swap(&arr[i], &arr[j]);     // 将当前元素交换到较小元素区末尾
        }
    }
    swap(&arr[i + 1], &arr[high]);  // 将基准元素交换到正确位置(i+1的位置)
    return i + 1;                   // 返回基准元素的最终位置
}

// 快速排序递归函数
void quickSort(int arr[], int low, int high) {  // 参数:数组,当前子数组起始和结束索引
    if (low < high) {  // 递归终止条件:当子数组长度大于1时执行
        int pi = partition(arr, low, high);  // 获取基准元素位置
        quickSort(arr, low, pi - 1);  // 递归排序基准左边的子数组
        quickSort(arr, pi + 1, high); // 递归排序基准右边的子数组
    }
}

// 打印数组的函数
void printArray(int arr[], int size) {  // 参数:数组和数组长度
    for (int i = 0; i < size; i++) {    // 遍历数组所有元素
        printf("%d ", arr[i]);          // 打印当前元素加空格
    }
    printf("\n");  // 打印换行符结束当前输出行
}

// 主函数
int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};  // 定义并初始化待排序数组
    int n = sizeof(arr) / sizeof(arr[0]);  // 计算数组长度:总字节数/单个元素字节数

    printf("原始数组: ");  // 打印原始数组提示信息
    printArray(arr, n);    // 调用打印函数显示原始数组
    
    quickSort(arr, 0, n - 1);  // 调用快速排序函数对数组进行排序

    printf("排序后的数组: ");  // 打印排序后数组提示信息
    printArray(arr, n);        // 调用打印函数显示排序结果

    return 0;  // 程序正常退出,返回0
}

问题验证:

  1. 快速排序的基本思想是什么?
  2. 如何选择基准元素?

二、归并排序:稳定且高效的选择

1. 归并排序的基本原理

归并排序(Merge Sort)也是一种分而治之的排序算法。它的基本思想是将数组分成两部分,递归地对每一部分进行排序,然后将两部分合并成一个有序的数组。归并排序的时间复杂度是O(n log n),并且是一个稳定的排序算法。

示例验证:归并排序的实现

#include <stdio.h>   // 包含标准输入输出库,用于printf等函数
#include <stdlib.h>  // 包含标准库(此代码中未实际使用,但通常用于动态内存分配)

// 合并两个已排序子数组的函数
void merge(int arr[], int left[], int right[], int leftSize, int rightSize) {
    int i = 0, j = 0, k = 0;  // 初始化三个索引指针:i-左数组,j-右数组,k-合并数组

    // 同时遍历左右子数组,比较元素并合并
    while (i < leftSize && j < rightSize) {  // 当两个子数组都有未处理元素时
        if (left[i] <= right[j]) {          // 如果左数组当前元素较小
            arr[k++] = left[i++];           // 取左数组元素放入合并数组,两个索引均递增
        } else {                            // 如果右数组当前元素较小
            arr[k++] = right[j++];          // 取右数组元素放入合并数组,两个索引均递增
        }
    }

    // 处理左数组剩余元素(如果有)
    while (i < leftSize) {         // 当左数组还有未处理元素时
        arr[k++] = left[i++];      // 将剩余元素依次放入合并数组
    }

    // 处理右数组剩余元素(如果有)
    while (j < rightSize) {        // 当右数组还有未处理元素时
        arr[k++] = right[j++];     // 将剩余元素依次放入合并数组
    }
}

// 归并排序主函数(递归实现)
void mergeSort(int arr[], int n) {  // 参数:待排序数组及其长度
    if (n <= 1) {                   // 递归终止条件:数组长度<=1时无需排序
        return;
    }

    int mid = n / 2;                // 计算数组中间分割点
    int left[mid];                  // 创建左子数组(C99变长数组)
    int right[n - mid];             // 创建右子数组(长度为总长度减去左子数组长度)

    // 将原数组前半部分复制到左子数组
    for (int i = 0; i < mid; i++) {  // 遍历原数组前半部分
        left[i] = arr[i];            // 复制元素到左子数组
    }

    // 将原数组后半部分复制到右子数组
    for (int i = mid; i < n; i++) {  // 遍历原数组后半部分
        right[i - mid] = arr[i];     // 复制元素到右子数组(右子数组索引从0开始)
    }

    mergeSort(left, mid);             // 递归排序左子数组
    mergeSort(right, n - mid);        // 递归排序右子数组
    merge(arr, left, right, mid, n - mid);  // 合并两个已排序的子数组
}

// 打印数组内容的工具函数
void printArray(int arr[], int size) {  // 参数:数组及其长度
    for (int i = 0; i < size; i++) {    // 遍历数组所有元素
        printf("%d ", arr[i]);         // 打印当前元素加空格分隔
    }
    printf("\n");                      // 打印换行符结束当前行
}

// 主函数
int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};  // 定义并初始化待排序数组
    int n = sizeof(arr) / sizeof(arr[0]);  // 计算数组长度:总字节数/单个元素字节数

    printf("原始数组: ");  // 打印提示信息
    printArray(arr, n);    // 调用打印函数显示原始数组

    mergeSort(arr, n);     // 调用归并排序函数对数组进行排序

    printf("排序后的数组: ");  // 打印结果提示信息
    printArray(arr, n);       // 调用打印函数显示排序结果

    return 0;  // 程序正常退出,返回0
}

 问题验证:

  1. 归并排序的实现步骤是什么?
  2. 归并排序的时间复杂度是多少?

三、堆排序:利用堆结构的高效排序

1. 堆排序的基本原理

堆排序(Heap Sort)利用了堆这种数据结构。堆是一个完全二叉树,其中每个父节点的值都大于或等于子节点的值(最大堆)或小于或等于子节点的值(最小堆)。堆排序的基本思想是将数组构建成一个堆,然后反复提取堆顶元素,将数组变成有序的。

示例验证:堆排序的实现

#include <stdio.h>  // 包含标准输入输出库,用于printf等函数

// 调整堆使其符合最大堆性质
void heapify(int arr[], int n, int i) {  // 参数:数组、数组长度、当前节点索引
    int largest = i;         // 初始化当前节点为最大值位置
    int left = 2 * i + 1;    // 计算左子节点索引(基于完全二叉树性质)
    int right = 2 * i + 2;   // 计算右子节点索引

    // 比较左子节点与当前最大值
    if (left < n && arr[left] > arr[largest]) {  // 确保左子节点在堆范围内
        largest = left;      // 更新最大值位置
    }

    // 比较右子节点与当前最大值
    if (right < n && arr[right] > arr[largest]) {  // 确保右子节点在堆范围内
        largest = right;     // 更新最大值位置
    }

    // 若最大值不在当前节点位置
    if (largest != i) {              
        swap(&arr[i], &arr[largest]);  // 交换当前节点与最大值节点的值
        heapify(arr, n, largest);      // 递归调整受影响的子树
    }
}

// 交换两个整数的值
void swap(int* a, int* b) {  // 接收两个整型指针
    int temp = *a;           // 暂存a指针指向的值
    *a = *b;                 // 将b的值赋给a指向的内存
    *b = temp;               // 将暂存值赋给b指向的内存
}

// 堆排序主函数
void heapSort(int arr[], int n) {  // 参数:数组及元素个数
    // 构建初始最大堆(从最后一个非叶子节点开始)
    for (int i = n / 2 - 1; i >= 0; i--) {  // i初始化为最后一个非叶子节点索引
        heapify(arr, n, i);         // 调整子树为最大堆
    }

    // 依次提取堆顶元素并调整堆
    for (int i = n - 1; i >= 0; i--) {  // 每次缩减堆范围
        swap(&arr[0], &arr[i]);     // 将堆顶最大值交换到数组末尾
        heapify(arr, i, 0);         // 调整剩余元素为最大堆(堆大小减1)
    }
}

// 打印数组内容
void printArray(int arr[], int size) {  // 参数:数组及元素个数
    for (int i = 0; i < size; i++) {    // 遍历数组
        printf("%d ", arr[i]);         // 打印元素加空格分隔
    }
    printf("\n");                      // 换行结束当前输出
}

// 主函数
int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};           // 初始化待排序数组
    int n = sizeof(arr) / sizeof(arr[0]);      // 计算数组长度(总字节数/单个元素字节数)

    printf("原始数组: ");          // 打印提示信息
    printArray(arr, n);            // 调用打印函数显示原始数组

    heapSort(arr, n);              // 执行堆排序

    printf("排序后的数组: ");       // 打印结果提示
    printArray(arr, n);            // 显示排序后结果

    return 0;                      // 程序正常退出
}

 四、性能对比与实际应用

算法时间复杂度空间复杂度稳定性适用场景
快速排序O(n log n)O(1)不稳定平均情况下的高效排序
归并排序O(n log n)O(n)稳定需要稳定排序的场景
堆排序O(n log n)O(1)不稳定原地排序,不需要额外空间的情况

示例验证:性能对比

#include <stdio.h>   // 包含标准输入输出库
#include <stdlib.h>  // 包含动态内存分配和随机数相关函数
#include <time.h>    // 包含时间测量相关函数

/* 以下排序算法实现已省略但需存在于程序中:
   - 快速排序 quickSort()
   - 归并排序 mergeSort()
   - 堆排序 heapSort() */

// 性能测试函数
void testPerformance(int n) {  // 参数n表示测试数据规模
    // 动态分配内存创建测试数组
    int* arr = (int*)malloc(n * sizeof(int));  // 分配n个整型的内存空间
    
    // 初始化随机数种子(注:实际应在main函数初始化一次)
    for (int i = 0; i < n; i++) {  // 遍历数组所有元素
        arr[i] = rand() % 1000;    // 生成0-999的随机数填充数组
    }

    // 定义计时相关变量
    clock_t start, end;         // clock_t类型用于存储处理器时间
    double cpu_time_used;       // 保存计算耗时

    /**************** 测试快速排序 ****************/
    start = clock();            // 记录开始时间点
    quickSort(arr, 0, n - 1);   // 调用快速排序(需实现)
    end = clock();              // 记录结束时间点
    cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC;  // 计算耗时(秒)
    printf("快速排序时间: %.6f 秒\n", cpu_time_used);  // 输出格式化时间

    /**************** 测试归并排序 ****************/
    // 重新生成随机数组(确保不同算法测试数据一致)
    for (int i = 0; i < n; i++) {  // 遍历数组所有元素
        arr[i] = rand() % 1000;    // 重新生成随机数
    }
    start = clock();            // 记录开始时间点
    mergeSort(arr, n);          // 调用归并排序(需实现)
    end = clock();              // 记录结束时间点
    cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC;
    printf("归并排序时间: %.6f 秒\n", cpu_time_used);

    /**************** 测试堆排序 ****************/
    for (int i = 0; i < n; i++) {  // 再次重置测试数据
        arr[i] = rand() % 1000;
    }
    start = clock();            // 记录开始时间点
    heapSort(arr, n);           // 调用堆排序(需实现)
    end = clock();              // 记录结束时间点
    cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC;
    printf("堆排序时间: %.6f 秒\n", cpu_time_used);

    free(arr);  // 释放动态分配的内存
}

// 主函数
int main() {
    int n = 10000;           // 定义测试数据规模(可修改数值进行不同量级测试)
    testPerformance(n);      // 调用性能测试函数
    
    return 0;                // 程序正常退出
}

问题验证:

  1. 快速排序、归并排序和堆排序的性能特点是什么?
  2. 如何根据需求选择合适的排序算法?

五、总结与实践建议

快速排序、归并排序和堆排序是C语言中非常重要的排序算法,它们各自有不同的特点和适用场景。快速排序适合平均情况下的高效排序,归并排序适合需要稳定排序的场景,堆排序适合不需要额外空间的原地排序。

实践建议:

  1. 在实际应用中,根据数据规模和排序需求选择合适的排序算法。
  2. 使用 profiling 工具(如Gprof)分析算法的性能表现。
  3. 阅读和分析优秀的C语言代码,学习排序算法的高级用法。

希望这篇博客能够帮助你深入理解C语言中的快速排序、归并排序和堆排序,提升编程能力。如果你有任何问题或建议,欢迎在评论区留言!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

司铭鸿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值