前言
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。其中二路归并类似二叉树的后序遍历。归并排序的思考更多的是解决在磁盘中的外排序问题。
主要思路:
■ 先将待排序的序列分割为小的序列,使每个子序列有序。
■ 将小的子序列合并为更大的子序列,即使子序列段间有序。合并到最后整个序列有序。
过程演示
递归实现归并排序
C语言代码实现
//将已有序的小区间合并为更大的有序区间,其中合并取得是闭区间。
void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int *temp) {
int index = begin1;
int left = begin1, right = end2;
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] < a[begin2]) temp[index++] = a[begin1++];
else temp[index++] = a[begin2++];
}
while(begin1<=end1)temp[index++] = a[begin1++];
while(begin2<=end2)temp[index++] = a[begin2++];
for (int i = left; i <= right;++i) a[i] = temp[i];
}
//递归处理更小的区间序列
void _MergeSort(int *a,int left,int right,int *temp) {
if (left >= right) return;//当区间只有一个元素或没有元素时返回
int mid = (left + right) >> 1;//以每个区间的中点分割区间
_MergeSort(a,left,mid,temp);//递归处理左区间
_MergeSort(a,mid+1,right,temp);//递归处理右区间
MergeArr(a,left,mid,mid+1,right,temp);//合并为更大的有序区间
}
//归并递归
void MergeSort(int *a,int n) {
assert(a);
int* temp = (int*)malloc(sizeof(int)*n);
if(temp==NULL){
printf("申请空间失败!!!\n");
exit(-1);
}
_MergeSort(a,0,n-1,temp);
free(temp);
}
非递归实现归并排序
过程如下图!!!
C语言代码实现
//将已有序的小区间合并为更大的有序区间,其中合并取得是闭区间。
void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int *temp) {
int index = begin1;
int left = begin1, right = end2;
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] < a[begin2]) temp[index++] = a[begin1++];
else temp[index++] = a[begin2++];
}
while(begin1<=end1)temp[index++] = a[begin1++];
while(begin2<=end2)temp[index++] = a[begin2++];
for (int i = left; i <= right;++i) a[i] = temp[i];
}
//归并非递归实现
void MergeSortNonR(int* a, int n) {
assert(a);
int* temp = (int*)malloc(sizeof(int)*n);
if(temp==NULL){
printf("申请空间失败!!!\n");
exit(-1);
}
int gap = 1;//其中gap的含义代表本次循环合并的数字个数
while (gap < n) {
for (int i = 0; i < n; i += 2 * gap) {
//因为是闭区间,所以end的大小要 -1;
int begin1=i, end1 =i+gap-1;
int begin2=i+gap, end2 =i+2*gap-1;
if (begin2 >= n) break;//当第二段区间不存在时,只存在第一段区间时,结束循环,如果只存在第一段区间则不用合并!!!!
if (end2 >= n) end2 = n - 1;//当第二段区间存在并且end2>=n 时缩小第二段区间的范围。
MergeArr(a,begin1,end1,begin2,end2,temp);
}
gap *= 2;
}
free(temp);
}
使用归并排序完成磁盘中的外排序
问题:当有一个文件中放置着以某种格式存在的海量数据,我们无法一次性将数据全部加载进入内存中,此时要求我们完成对这个文件的排序。
思路:
■ 将大文件先分割成多个小文件。
■ 将大文件中的部分数据加载进入内存中,为了节省空间我们可以使用快速排序,将这部分数据排序。然后将排好的数据输出到小文件中。
■ 重复上述过程直到将大文件中的数据全部形成小文件。
■ 合并小文件形成更大的文件,然后将这个更大的文件接着和剩下的小文件进行合并。重复此过程直到小文件最终被合并完毕,这样就形成了最终排好序的大文件。
过程实现如下图!!!
C语言代码实现
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
void MergeFile(const char*sin1,const char *sin2,const char*sout) {
FILE* file1 = fopen(sin1,"r");
if (file1 == NULL) {
printf("file1打开文件失败!!\n");
exit(-1);
}
FILE* file2 = fopen(sin2, "r");
if (file2 == NULL) {
printf("file2打开文件失败!!\n");
exit(-1);
}
FILE* fout = fopen(sout,"w");
if (fout == NULL) {
printf("fout打开文件失败!!\n");
exit(-1);
}
int num1, num2;
int flag1 = fscanf(file1,"%d ",&num1);
int flag2 = fscanf(file2, "%d ", &num2);
while (flag1!=EOF&&flag2!= EOF) {
if (num1 < num2) {
fprintf(fout,"%d ",num1);
flag1 = fscanf(file1, "%d ", &num1);
}
else {
fprintf(fout, "%d ", num2);
flag2 = fscanf(file2, "%d ", &num2);
}
}
while (flag1 != EOF) {
fprintf(fout, "%d ", num1);
flag1 = fscanf(file1, "%d ", &num1);
}
while (flag2 != EOF) {
fprintf(fout, "%d ", num2);
flag2 = fscanf(file2, "%d ", &num2);
}
fclose(file1);
fclose(file2);
fclose(fout);
}
//file文件的名字,N生成小文件的数量,Nnum一个小文件中包含多少个数
void MergeSortFile(const char* file, const int N, const int Nnum) {
FILE* fout = fopen(file, "r"); //读取这个大文件
int* temp = (int*)malloc(sizeof(int) * Nnum); //保存从文件中输出的数据
if (temp == NULL) {
printf("内存不足、申请失败!!\n");
exit(-1);
}
char subfile[20]; //保存每个小文件的文件名
int i = 0, filei = 1;//i 用作数组索引,filei代表第几个小文件
int num; //保存从文件中读入的每个数据
while (fscanf(fout, "%d ", &num) != EOF) {
if (i < Nnum - 1) {//i<Nnum-1
temp[i++] = num;//将从大文件中读入的数据保存到数组中
}
else {
temp[i] = num;
QuickSortNonR(temp, 0, Nnum - 1);//用快速排序将数组的数据进行排序,这个函数需要自己写。
sprintf(subfile, "sub\\subsort%d", filei++);//生成文件名
FILE* fin = fopen(subfile, "w");//打开小文件
if (fin == NULL) {
printf("打开文件失败!!!");
exit(-1);
}
for (int j = 0; j < Nnum; j++) {
fprintf(fin, "%d ", temp[j]);//将数组中的数据保存到小文件中
}
fclose(fin);
i = 0;
printf("subsort%d已完成排序!!!!\n",filei-1);
}
}
printf("小文件排序完成!!!!\n");
const char prefixout[50] = "sub\\mergefile";
const char prefixin[50] = "sub\\subsort";
char fmout[50]="sub\\mergefile1";
char fin1[50]="sub\\subsort1";
char fin2[50]="sub\\subsort2";
for (int i = 2; i <= N; i++) {
MergeFile(fin1,fin2, fmout);
printf("%s合并完毕\n", fmout);
strcpy(fin1, fmout);
strcpy(fmout, prefixout);
strcpy(fin2, prefixin);
sprintf(fmout,"%s%d%s",fmout,i);
sprintf(fin2,"%s%d%s",fin2,i+1);
}
fclose(fout);
}
int main() {
srand(time(0));
char sfile[30] = "e:\\random.txt";
const int N=10,Nnum= 5000000;
FILE* file = fopen(sfile, "w");
if (file == NULL) {
printf("open fail!!!\n");
exit(-1);
}
else {
for (int i = 0; i < N; i++) {
for (int j = 0; j < Nnum; j++) {
fprintf(file, "%d ", rand());
}
}
fclose(file);
}
//以上过程实现生成一个大文件,
printf("生成随机数完成!!!!\n");
printf("小文件开始分解排序!!!!\n");
MergeSortFile(sfile,N,Nnum);
printf("小文件合并完成!!!!\n");
return 0;
}
归并排序的特性总结
1、归并排序的缺点在于需要额外的O(n)的空间,空间复杂度:O(n)。但是有个优点就是可以用作外排序,其中使用外排序实际就是把文件当作数组来进行。我们用归并排序实现外排序可以解决磁盘中的外排序。
2、时间复杂度:O(nlongn),其中最好和最坏时间复杂度都是O(nlogn)。
3、稳定性:稳定。