目录
1. 概念
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:


2. 归并排序递归版本的实现
// 归并排序递归实现
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin == end) //如果区间只有一个值,直接返回
{
return;
}
int mid = (begin + end) / 2;
//[begin, mid - 1],[mid, end]这种分割方式会进入死循环,因为除法本事就会丢数据,当为[偶数,
//偶数+1]区间时,分出来的右区间还是[偶数,偶数+1]
//[begin, mid], [mid+1, end]
//如果[begin, mid], [mid + 1, end]有序,就可以进行归并了
//使左右区间有序,进行子问题递归
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
//归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//拷贝时一段一段拷贝,拷贝每一段的[begin, end]区间
memcpy(a+begin, tmp+begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
//左区间和右区间有序,借助一个新的数组,取小的尾插
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(1);
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
}
3. 归并排序非递归版本的实现
归并排序的非递归实现,就数组从头开始先一一归并,然后再二二归并,然后再四四归并,依次类推。归并排序非递归版本中出现的问题我已经写在了代码中的注释中,也可以将注释的printf语句取消注释,观察每次非递归模拟递归的区间。
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(1);
}
//gap是每组归并数据子区间的数据个数
//11归并,gap == 1
//则每组归并数据子区间的数据个数为1
int gap = 1;
while (gap < n)
{
//printf("gap = %d->:", gap);
for (int i = 0; i < n; i += 2 * gap) //11归并,则到下一组归并时要跳2个gap
{
//[begin1, end1],[begin2, end2]
int begin1 = i, end1 = begin1 + gap - 1; //闭区间的结束位置为[左 + gap - 1]
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
//printf("[%d, %d],[%d, %d];", begin1, end1, begin2, end2);
//存在的越界情况,以10个数据为例
//打印结果:
//gap = 1->:[0, 0],[1, 1];[2, 2],[3, 3];[4, 4],[5, 5];[6, 6],[7, 7];[8, 8],[9,
//9];
//gap = 2->: [0, 1] , [2, 3];[4, 5], [6, 7];[8, 9], [10, 11];
//gap = 4-> : [0, 3] , [4, 7];[8, 11], [12, 15];
//gap = 8-> : [0, 7] , [8, 15];
//情况1:gap == 8的区间[8, 15],只有end2越界
//情况2:gap == 2的区间[10, 11],begin2和end2越界
//情况3:gap == 4的区间[8, 11],[12, 15] end1,begin2和end2越界
//上述的情况2和情况3可以合为1种情况(则[begin2,end2]区间不存在),该情况下不需要归并
//这个判断语句处理的是情况2和情况3,当begin2越界的时候,不用处理,当end1越界的时
//候,begin2也越界了,也不用处理
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
//printf("[%d, %d],[%d, %d];", begin1, end1, begin2, end2);
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//写end2 - i + 1,是因为在归并的过程中begin1在改变
memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
}
//printf("\n");
gap *= 2;
}
free(tmp);
tmp = NULL;
}
4. 归并排序的特性总结
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
5. 外排序及文件归并排序
5.1 外排序介绍
外排序(External sorting)是针对无法一次性加载到内存的海量数据的排序算法,核心思想是 “分而治之”,将大规模数据拆分为多个可加载到内存的小数据块,在内存中完成排序后,再通过归并操作合并为有序的完整数据。
与内排序(如快速排序、堆排序,数据全部在内存中处理)不同,外排序的操作对象是外部存储设备(硬盘、磁带等)上的海量数据,其性能瓶颈主要在于减少磁盘 I/O 次数(磁盘读写速度远低于内存操作)。
外排序的标准流程分为 两个核心阶段:内存排序(分块) 和 多路归并(合并)。
5.2 文件归并排序思路
- 原始文件中依次读取 n 个值排序后写入 file1 中。
- 原始文件中依次读取 n 个值排序后写入 file 2 中。
- file1 和 file2 利用归并排序的思想,依次读取比较,取小的尾插到 mfile 中,则 mfile 归并为一个有序文件。
- 将 file1 和 file2 删掉,mfile 重命名为 file1。
- 重复上述第 2,3,4步,直到原始文件数据全部读完,最后归并出的有序数据放到 file1 中。

5.3 文件归并排序的代码实现
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
void Swap(int* p1, int* p2) {
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 创建 n 个随机数,写入到文件中
void CreateNDate(int n) {
srand((unsigned int)time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL) {
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i) {
int x = rand() + i;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
// 归并排序
void _MergeSort(int* a, int* tmp, int begin, int end) {
if (begin >= end)
return;
// 分区间
int mid = (begin + end) / 2;
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] < a[begin2])
tmp[i++] = a[begin1++];
else
tmp[i++] = a[begin2++];
}
while (begin1 <= end1)
tmp[i++] = a[begin1++];
while (begin2 <= end2)
tmp[i++] = a[begin2++];
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* arr, int n) {
if (n <= 1)
return;
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL) {
perror("malloc failed!\n");
exit(-1);
}
_MergeSort(arr, tmp, 0, n - 1);
}
// 从原始文件 fout 中读取 n 个数据,排序后写入到 file 中。返回值为实际读取的数据个数
int ReadNNumSortToFile(FILE* fout, int* a, int n, const char* file) {
int x = 0;
int i = 0;
while (i < n && fscanf(fout, "%d", &x) != EOF)
a[i++] = x;
// 一个数据都没读到返回0,说明本次已经是从文件的末尾读了
if (i == 0)
return i;
// 单文件的内排序使用归并排序
MergeSort(a, i);
// 打开待写入排序好的元素的文件
FILE* fin = fopen(file, "w");
if (fin == NULL) {
printf("打开%s文件失败\n", file);
exit(-1);
}
// 写入数据
for (int j = 0; j < i; ++j)
fprintf(fin, "%d\n", a[j]);
fclose(fin);
return i;
}
void MergeSortedFile(const char* file1, const char* file2, const char* mfile) {
FILE* fout1 = fopen(file1, "r");
if (fout1 == NULL) {
printf("打开%s文件失败\n", file1);
exit(-1);
}
FILE* fout2 = fopen(file2, "r");
if (fout2 == NULL) {
printf("打开%s文件失败\n", file2);
exit(-1);
}
FILE* fin = fopen(mfile, "w");
if (fin == NULL) {
printf("打开%s文件失败\n", mfile);
exit(-1);
}
int num1, num2;
// ret1,ret2 表示读取到的个数
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF) {
if (num1 < num2) {
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
} else {
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
}
while (ret1 != EOF) {
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF) {
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);
fclose(fout2);
fclose(fin);
}
// n 表示每个数据块的个数
void MergeSortFile(const char* file, int n) {
// 打开原始待排序文件 file
FILE* fout = fopen(file, "r");
if (fout == NULL) {
printf("打开%s文件失败\n", file);
exit(-1);
}
int i = 0;
int x = 0;
const char* file1 = "file1.txt";
const char* file2 = "file2.txt";
const char* mfile = "mfile.txt";
int* a = (int*)malloc(sizeof(int) * n);
if (a == NULL) {
perror("malloc failed!\n");
exit(-1);
}
// 从原始文件中分别读取 n 个数据排序后写入 file1,file2
ReadNNumSortToFile(fout, a, n, file1);
ReadNNumSortToFile(fout, a, n, file2);
while (1) {
// 将 file1,file2 中的数据归并到 mfile 中
MergeSortedFile(file1, file2, mfile);
// 删除 file1,file2
if (remove(file1) != 0 || remove(file2) != 0) {
perror("remove file failed!\n");
exit(-1);
}
// 将 mifle 重命名为 file1
if (rename(mfile, file1) != 0) {
perror("rename filename failed!\n");
exit(-1);
}
// 读取 n 个数据到 file2,继续归并,没数据则归并结束
if (ReadNNumSortToFile(fout, a, n, file2) == 0)
break;
}
printf("%s文件成功排序到%s\n", file, file1);
fclose(fout);
free(a);
}
int main() {
CreateNDate(1000000);
MergeSortFile("data.txt", 100000);
return 0;
}
注:不管逻辑上分为多少个数据块,在物理层面,文件IO(每次 4KB)的次数都是不变的,物理层面每次文件IO都有“找磁道,找扇区”的过程,但是如果数据块较大的话,操作系统会做连续预读优化,一次IO会预读多个缓冲区的数据(如 8 个 4KB),从而减少文件IO次数。
3万+



被折叠的 条评论
为什么被折叠?



