排序算法是计算机科学中不可或缺的一部分,用于将一组数据按照特定顺序排列。在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. 归并排序的基本原理
归并排序(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. 堆排序的基本原理
堆排序(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; // 程序正常退出
}
问题验证:
- 快速排序、归并排序和堆排序的性能特点是什么?
- 如何根据需求选择合适的排序算法?
五、总结与实践建议
快速排序、归并排序和堆排序是C语言中非常重要的排序算法,它们各自有不同的特点和适用场景。快速排序适合平均情况下的高效排序,归并排序适合需要稳定排序的场景,堆排序适合不需要额外空间的原地排序。
实践建议:
- 在实际应用中,根据数据规模和排序需求选择合适的排序算法。
- 使用 profiling 工具(如Gprof)分析算法的性能表现。
- 阅读和分析优秀的C语言代码,学习排序算法的高级用法。
希望这篇博客能够帮助你深入理解C语言中的快速排序、归并排序和堆排序,提升编程能力。如果你有任何问题或建议,欢迎在评论区留言!