归并排序,外排序
归并排序
基本思想
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
思维导图
特性总结
-
归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
-
时间复杂度:O(N*logN)
-
空间复杂度:O(N)
-
稳定性:稳定
代码实现(递归)
//归并排序单趟思想:合并两段有序的数组,合并以后依旧有序
void _mergesort(int* a, int left, int right, int* tmp)
{
//递归终止条件,当区间不存在或者只剩下一个数,递归终止
if (right <= left)
{
return;
}
int mid = left + (right - left) / 2;
//[left,mid][mid+1,right] 有序,则可以合并,若无序,用子问题解决
_mergesort(a, left, mid, tmp);
_mergesort(a, mid + 1, right, tmp);
//归并[left,mid][mid+1,right] 有序
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
//i tmp数组的下标
int i = left;
while (begin1<=end1&&begin2<=end2)
{
//排升序,将小的数放在前面
if (a[begin1]<a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//将还没走到最后面的那个数组剩下的值全部放到tmp中
while (begin1<=end1)
{
tmp[i++] = a[begin1++];
}
while (begin2<=end2)
{
tmp[i++] = a[begin2++];
}
//将归并好的区间拷贝回原数组
//归并好的区间:begin1——end2
for (int j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
//归并排序
//时间复杂度:O(N*logN)
//深度为logN 每一层都有N个数被归并到上一层,故时间复杂度为O(N*logN)
void mergesort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_mergesort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
归并排序非递归思维导图
代码实现(非递归)
//归并排序(非递归)
//一组归并完,整体拷贝回原数组
//end1和begin2越界都需要归并
void mergesortnotR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap<n)
{
//[i,i+gap-1] [i+gap,i+2*gap-1]
for (int i = 0; i < n; i+=2*gap)
{
int begin1 = i, end1 = i+gap-1;
int begin2 = i+gap, end2 = i+2*gap-1;
int index = i;
//end1越界
if (end1>=n)
{
end1 = n - 1;
}
//begin2越界
if (begin2>=n)
{
begin2 = n;
end2 = n - 1;
//使这个区间不存在,防止tmp数组越界
}
//end2越界
if (end2>=n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
//排升序,将小的数放在前面
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//将还没走到最后面的那个数组剩下的值全部放到tmp中
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
}
//一组归并完,整体拷贝回原数组
for (int j = 0; j < n; j++)
{
a[j] = tmp[j];
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
//归并排序(非递归)
//归一部分,拷贝一部分回原数组
//end1和begin2越界都不需要归并
void mergesortnotR1(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
//[i,i+gap-1] [i+gap,i+2*gap-1]
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int index = i;
//end1和begin2越界都不需要归并,直接跳出循环
if (end1>=n||begin2>=n)
{
break;
}
//end2越界,需要归并
if (end2>=n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
//排升序,将小的数放在前面
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//将还没走到最后面的那个数组剩下的值全部放到tmp中
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
//归一部分,拷贝一部分回原数组
for (int j = i; j <=end2; j++)
{
a[j] = tmp[j];
}
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
外排序
基本思想
当文件中待排序数据空间超过计算机内存的大小时,可以将文件数据切分为N块,每一块数据大小与内存一样大,
再用快速排序对每一块数据进行排序,最后,在文件中用归并排序将N块有序的数据归并到原来的文件中。
思维导图
代码实现
void _mergefile(const char* file1, const char* file2, const char* mfile)
{
//FILE * fopen ( const char * filename, const char * mode );
//filename: 文件名
//mode:打开的方式
//r 以读的方式打开 w 以写的方式打开
FILE* fin1 = fopen(file1, "r");
FILE* fin2 = fopen(file2, "r");
FILE* fout = fopen(mfile, "w");
int num1, num2;
//读写文件的时候,文件指针都会自动往后移动
//fscanf 将fin1文件中的数据读取到num1中
int ret1=fscanf(fin1, "%d\n", &num1);
int ret2=fscanf(fin2, "%d\n", &num2);
while (ret1!=EOF&&ret2!=EOF)
{
if (num1<num2)
{
//fprintf 将num1中的数据写入到fout文件中
fprintf(fout, "%d\n", num1);
ret1 = fscanf(fin1, "%d\n", &num1);
}
else
{
fprintf(fout, "%d\n", num2);
ret2 = fscanf(fin2, "%d\n", &num2);
}
}
while (ret1!=EOF)
{
fprintf(fout, "%d\n", num1);
ret1 = fscanf(fin1, "%d\n", &num1);
}
while (ret2!=EOF)
{
fprintf(fout, "%d\n", num2);
ret2 = fscanf(fin2, "%d\n", &num2);
}
//打开文件后,需要用close关闭文件
fclose(fin1);
fclose(fin2);
fclose(fout);
}
//外排序
void mergesortfile(const char*file)
{
//分为10 组
int n = 10;
FILE* fout = fopen(file, "r");
assert(fout);
int num = 0;
//每组存放10个数据
int a[10];
//数组a的下标
int i = 0;
//文件名下标
int filei = 1;
//存放文件名的数组
char subfile[20];
//读取数据过程中,若为EOF,则表示文件读取结束
while (fscanf(fout,"%d\n",&num)!=EOF)
{
//每读取一个数,文件指针都会移动一下
//if中读9个数,else中再读一个数
//因为无论执行if还是执行else 文件都会读取数据
//若if中读取10个数,在执行else时不读取数的话,每次读就会丢失一个数
if (i<n-1)
{
a[i++] = num;
}
else
{
a[i] = num;
//对每组数进行快排,使其变得有序
quicklysort(a,0,n - 1);
//sprintf 将filei转化为字符串,并存入subfile数组
sprintf(subfile, "ssort\\sub_sort%d", filei++);
//排完序后,将有序的这个序列写入文件中
FILE* fin = fopen(subfile, "w");
assert(fin);
for (int i = 0; i < n; i++)
{
fprintf(fin, "%d\n", a[i]);
}
fclose(fin);
i = 0;
}
}
//利用互相归并到文件,实现整体有序
char file1[100]= "ssort\\sub_sort1";
char mfile[100]="ssort\\sub_sort12";
char file2[100];
for (int i = 2; i <= n; i++)
{
sprintf(file2, "ssort\\sub_sort%d", i);
//读取file1和file2,进行归并处mfile
_mergefile(file1, file2, mfile);
//迭代
strcpy(file1, mfile);
sprintf(mfile, "%s%d", mfile, i + 1);
}
fclose(fout);
}
总结
归并排序的核心思想是:将两个有序的数组进行归并,归并后还有序。对于无序的数组,可先用分治的思想,将其分为不可再分的子区间后,再进行归并,从而达到整体有序。归并的非递归可以用栈或者队列来实现,实现方法于快排的非递归法差不多,都是用栈或者队列保存子区间下标,但也可以不用栈和队列,可直接用gap来表示每次递归的间距,通过循环使gap变化,也能产生同样的效果。外排序这一块,主要逻辑到不是很难,只是对文件的操作比较生疏,得加强这一方面的练习!