C语言实现归并排序详解:原理、代码与性能分析
归并排序(Merge Sort)是一种基于分治思想的稳定排序算法。本文详细介绍其在C语言中的实现原理,包括核心的“合并”操作、动态内存分配以及常见的内存错误处理。重点对比自顶向下递归和自底向上迭代两种实现路径,并给出对应的完整代码示例,最后分析其时间与空间复杂度。
结果与目标
通过本文学习,你将掌握如何在C语言中高效实现归并排序,理解如何通过辅助数组完成有序子序列的合并,以及如何避免频繁的内存分配开销。
准备条件
在开始实现之前,需要明确归并排序的核心依赖:
- 核心概念:将待排序数组视为由多个长度为1的子文件组成,两两合并,直到生成一个完整的有序文件。
- 存储结构:适用于顺序存储结构(如C语言数组),也易于在链表上实现。
- 关键函数:需要一个`merge`(或`Merge`)函数来处理两个有序区间的合并。
真实操作顺序
第1步:实现基础的合并逻辑
目的:将两个相邻的有序子区间 `[low..mid]` 和 `[mid+1..high]` 合并为一个有序区间。
动作:
- 动态申请一个与原区间等长的临时数组 `newarr`。
- 设置指针 `i`、`j` 分别指向两个子区间的起始位置,`p` 指向临时数组起始位置。
- 比较 `arr[i]` 与 `arr[j]`,将较小者复制到 `newarr[p]`,并移动相应指针。
- 当一个子区间遍历完毕后,将另一个子区间的剩余元素直接复制到 `newarr`。
- 将 `newarr` 的内容拷贝回原数组 `arr[low..high]`,并释放临时内存。
重要参数:输入数组 `arr[]`,左右边界 `low, mid, high`。
// 合并将两个有序序列([low,mid],[mid+1,high])合并
void Merge(int arr[], int low, int mid, int high) {
int i = low, j = mid + 1, p = 0;
// 动态申请暂存数组
int *newarr = (int *)malloc((high - low + 1) * sizeof(int));
if (!newarr) {
printf("malloc error!\n");
exit(1);
}
// 依次比较并复制较小值
while (i <= mid && j <= high) {
if (arr[i] < arr[j])
newarr[p++] = arr[i++];
else
newarr[p++] = arr[j++];
}
// 处理剩余元素
while (i <= mid)
newarr[p++] = arr[i++];
while (j <= high)
newarr[p++] = arr[j++];
// 将结果复制回原数组
for (i = low, p = 0; p < (high - low + 1); i++, p++)
arr[i] = newarr[p];
free(newarr);
}
第2步:实现自顶向下的递归排序
目的:使用分治法,将大数组不断二分,直到子数组长度为1,然后逐步合并。
动作:
- 在主函数 `merge_sort` 中一次性分配辅助数组 `tmp`,以避免递归过程中反复申请释放内存。
- 调用递归函数 `msort`,计算中间点 `center`,分别对左半部分和右半部分进行递归排序。
- 递归结束后,调用 `merge` 或 `merge1` 将排好序的两部分合并。
- 在递归结束前释放一次性分配的 `tmp`。
注意:代码中使用了 `assert` 检查空指针和非法参数,增强鲁棒性。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
static void merge1(int array[], int tmp[], int lpos, int rpos, int rend);
static void msort1(int array[], int tmp[], int left, int right);
void merge_sort1(int array[], int n) {
assert(array != NULL && n > 1);
int *tmp = (int *)malloc(sizeof(int) * n);
assert(tmp != NULL);
// 备份到tmp,方便后续交替使用
for (int i = 0; i < n; i++) {
tmp[i] = array[i];
}
msort1(array, tmp, 0, n - 1);
free(tmp);
}
// 递归划分
static void msort1(int array[], int tmp[], int left, int right) {
assert(array != NULL && tmp != NULL);
if (left == right)
return;
int center = (left + right) / 2;
// 注意这里的参数传递:先排tmp再copy回array,或者反之
// 此版本逻辑:msort1将tmp的数据按顺序整理,最终存入array
msort1(tmp, array, left, center);
msort1(tmp, array, center + 1, right);
merge1(tmp, array, left, center + 1, right);
}
// 合并操作
static void merge1(int array[], int tmp[], int lpos, int rpos, int rend) {
assert(array != NULL && tmp != NULL);
int lend = rpos - 1;
int tmp_pos = lpos;
// 核心比较逻辑
while (lpos <= lend && rpos <= rend) {
if (array[lpos] <= array[rpos])
tmp[tmp_pos++] = array[lpos++];
else
tmp[tmp_pos++] = array[rpos++];
}
// 处理剩余段
while (lpos <= lend)
tmp[tmp_pos++] = array[lpos++];
while (rpos <= rend)
tmp[tmp_pos++] = array[rpos++];
}
int main(int argc, char *argv[]) {
int a[7] = {6, 5, 4, 3, 2, 1, 7};
merge_sort1(a, 7);
for (int i = 0; i < 7; i++) {
printf("%3d", a[i]);
}
printf("\n");
return 0;
}
第3步:实现自底向上的迭代排序
目的:不使用递归,通过循环控制子文件的长度 `length`(1, 2, 4...),逐步合并整个数组。这种方式避免了递归调用的栈开销。
动作:
- 初始化 `length = 1`。
- 循环执行:每次遍历数组,以 `2*length` 为步长,将相邻的两个长度为 `length` 的子文件合并。
- 特殊处理:如果子文件个数为奇数,最后一个子文件不参与本轮合并;如果最后一段长度不足 `length`,也要进行合并处理。
- 当 `length >= n` 时,排序完成。
注意:`MergePass` 函数负责单趟归并,需仔细处理索引越界和最后一组的特殊情况。
/* 自底向上归并排序 */
void MergePass(SeqList R, int length) {
// 这里假设 SeqList 包含 data 和 length 成员,实际实现需根据具体结构体调整
// 伪代码逻辑演示:
int i;
for (i = 0; i + 2 * length - 1 < n; i += 2 * length)
Merge(R, i, i + length - 1, i + 2 * length - 1);
// 处理最后剩余的子文件
if (i + length - 1 < n)
Merge(R, i, i + length - 1, n - 1);
}
void MergeSort(SeqList R) {
int length;
for (length = 1; length < n; length *= 2)
MergePass(R, length);
}
第4步:另一种常见的原地合并变体
目的:简化合并函数的接口,减少参数传递。此方法通常在递归主流程中预先分配 `tmp` 数组,合并时直接从 `array` 读,写入 `tmp`,最后再拷回 `array`。
动作:
void merge(int array[], int tmp[], int lpos, int rpos, int rend) {
int i, leftend, num, tmppos;
leftend = rpos - 1;
num = rend - lpos + 1;
tmppos = lpos;
while (lpos <= leftend && rpos <= rend) {
if (array[lpos] <= array[rpos])
tmp[tmppos++] = array[lpos++];
else
tmp[tmppos++] = array[rpos++];
}
while (lpos <= leftend)
tmp[tmppos++] = array[lpos++];
while (rpos <= rend)
tmp[tmppos++] = array[rpos++];
// 从后往前拷贝,避免覆盖未读取的数据
for (i = 0; i < num; i++, rend--)
array[rend] = tmp[rend];
}
完成判断与分析
归并排序具有以下显著特征:
- 稳定性:归并排序是稳定的排序算法。如果相等元素的相对位置在排序前后保持一致,则称为稳定。由于合并时优先选取左侧元素,因此具有稳定性。
- 时间复杂度:无论最好、最坏还是平均情况,归并排序的时间复杂度均为
O(n log n)。这是因为每一层归并都需要遍历所有n个元素,共有log n层。 - 空间复杂度:需要额外的
O(n)辅助空间来暂存合并结果,不是就地排序。这是其主要缺点,特别是在内存受限的环境中。 - 适用性:适合数据量较大且对稳定性有要求的场景。如果是链表排序,归并排序可以做到几乎原地排序(仅需少量额外指针空间)。
简要总结
归并排序通过“分而治之”的策略实现了高效的排序。在C语言实现中,核心难点在于正确管理临时数组的生命周期以及处理合并过程中的边界条件。虽然它牺牲了空间换取了稳定的 O(n log n) 时间效率,但因其性能的可预测性,成为工业级排序库中的重要组成部分。
以上就是C语言实现归并排序的详细内容包括原理分析、三种主要代码实现及性能评估,更多关于C语言算法优化的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













