归并排序
我们可以看一下图来自己先理解一下归并排序
归并排序是采用分治法的一个非常典型的应用。其基本思想是:将已有序的子序合并,从而得到完全有序的序列,即先使每个子序有序,再使子序列段间有序。
相信大家都知道如何将两个有序序列合为一个有序序列吧:
那么如何得到有序的子序列呢?当序列分解到只有一个元素或是没有元素时,就可以认为是有序了,这时分解就结束了,开始合并:
递归实现
归并排序,从其思想上看就很适合使用递归来实现,并且用递归实现也比较简单。其间我们需要申请一个与待排序列大小相同的数组用于合并过程两个有序的子序列,合并完毕后再将数据拷贝回原数组。
代码:
cpp
//归并排序(子函数)
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)//归并结束条件:当只有一个数据或是序列不存在时,不需要再分解
{
return;
}
int mid = left + (right - left) / 2;//中间下标
_MergeSort(a, left, mid, tmp);//对左序列进行归并
_MergeSort(a, mid + 1, right, tmp);//对右序列进行归并
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
//将两段子区间进行归并,归并结果放在tmp中
int i = left;
while (begin1 <= end1&&begin2 <= end2)
{
//将较小的数据优先放入tmp
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++];
//归并完后,拷贝回原数组
int j = 0;
for (j = left; j <= right; j++)
a[j] = tmp[j];
}
//归并排序(主体函数)
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与原数组大小相同的空间
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);//归并排序
free(tmp);//释放空间
}
非递归实现
归并排序的非递归算法并不需要借助栈来完成,我们只需要控制每次参与合并的元素个数即可,最终便能使序列变为有序:
当然,以上例子是一个待排序列长度比较特殊的例子,我们若是想写出一个广泛适用的程序,必定需要考虑到某些极端情况:
情况一:
当最后一个小组进行合并时,第二个小区间存在,但是该区间元素个数不够gap个,这时我们需要在合并序列时,对第二个小区间的边界进行控制。
情况二:
当最后一个小组进行合并时,第二个小区间不存在,此时便不需要对该小组进行合并。
情况三:
当最后一个小组进行合并时,第二个小区间不存在,并且第一个小区间的元素个数不够gap个,此时也不需要对该小组进行合并。(可与情况二归为一类)
只要把控好这三种特殊情况,写出归并排序的非递归算法便轻而易举了。
代码:
cpp
//归并排序(子函数)
void _MergeSortNonR(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int j = begin1;
//将两段子区间进行归并,归并结果放在tmp中
int i = begin1;
while (begin1 <= end1&&begin2 <= end2)
{
//将较小的数据优先放入tmp
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++];
//归并完后,拷贝回原数组
for (; j <= end2; j++)
a[j] = tmp[j];
}
//归并排序(主体函数)
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与待排序列大小相同的空间,用于辅助合并序列
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;//需合并的子序列中元素的个数
while (gap < n)
{
int i = 0;
for (i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (begin2 >= n)//最后一组的第二个小区间不存在或是第一个小区间不够gap个,此时不需要对该小组进行合并
break;
if (end2 >= n)//最后一组的第二个小区间不够gap个,则第二个小区间的后界变为数组的后界
end2 = n - 1;
_MergeSortNonR(a, tmp, begin1, end1, begin2, end2);//合并两个有序序列
}
gap = 2 * gap;//下一趟需合并的子序列中元素的个数翻倍
}
free(tmp);//释放空间
}
外排序
外排序
首先,我先说明一下什么是内排序,什么是外排序:
内排序:数据量相对少一些,可以放到内存中进行排序。
外排序:数据量较大,内存中放不下,数据只能放到磁盘文件中,需要排序。
上面介绍的排序算法均是在内存中进行的,对于数据量庞大的序列,上面介绍的排序算法都束手无策,而归并排序却能胜任这种海量数据的排序。
假设现在有10亿个整数(4GB)存放在文件A中,需要我们进行排序,而内存一次只能提供512MB空间,归并排序解决该问题的基本思路如下:
1、每次从文件A中读取八分之一,即512MB到内存中进行排序(内排序),并将排序结果写入到一个文件中,然后再读取八分之一,重复这个过程。那么最终会生成8个各自有序的小文件(A1~A8)。
2、对生成的8个小文件进行11合并,最终8个文件被合成为4个,然后再11合并,就变成2个文件了,最后再进行一次11合并,就变成1个有序文件了。
注意:这里将两个文件进行11合并,并不是先将两个文件读入内存然后进行合并,因为内存装不下。这里的11合并是利用文件输入输出函数,从两个文件中各自读取一个数据,然后进行比较,将较小的数据写入到一个新文件中去,然后再读取,再比较,再写入,最终将两个文件中的数据全部写入到另一个文件中去,那么此时这个文件又是一个有序的文件了。
当然,你也可以这样合并文件:
外排序代码示例:
cpp
//将file1文件和file2文件中的数据归并到mfile文件中
void _MergeFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, "r");//打开file1文件
if (fout1 == NULL)
{
printf("打开文件失败\n");
exit(-1);
}
FILE* fout2 = fopen(file2, "r");//打开file2文件
if (fout2 == NULL)
{
printf("打开文件失败\n");
exit(-1);
}
FILE* fin = fopen(mfile, "w");//打开mfile文件
if (fin == NULL)
{
printf("打开文件失败\n");
exit(-1);
}
int num1, num2;
int ret1 = fscanf(fout1, "%d\n", &num1);//读取file1文件中的数据
int ret2 = fscanf(fout2, "%d\n", &num2);//读取file2文件中的数据
while (ret1 != EOF && ret2 != EOF)
{
//将读取到的较小值写入到mfile文件中,继续从file1和file2中读取数据进行比较
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);
}
}
//将file1文件中未读取完的数据写入文件mfile中
while (ret1 != EOF)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
//将file2文件中未读取完的数据写入文件mfile中
while (ret2 != EOF)
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);//关闭file1文件
fclose(fout2);//关闭file2文件
fclose(fin);//关闭mfile文件
}
//将文件中的数据进行排序
void MergeSortFile(const char* file)
{
FILE* fout = fopen(file, "r");//打开文件
if (fout == NULL)
{
printf("打开文件失败\n");
exit(-1);
}
// 分割成一段一段数据,内存排序后写到小文件中
int n = 10;//一次读取10个数据进行内排序
int a[10];//读取数据放到数组中,准备进行内排序
int i = 0;
int num = 0;
char subfile[20];//文件名字符串
int filei = 1;//储存第filei组内排序后的数据的文件的文件名
memset(a, 0, sizeof(int)*n);//将数组a的空间置0
while (fscanf(fout, "%d\n", &num) != EOF)//从文件中读取数据
{
if (i < n - 1)//读取前9个数据
{
a[i++] = num;
}
else//读取第10个数据
{
a[i] = num;
QuickSort(a, 0, n - 1);//将这10个数据进行快速排序
sprintf(subfile, "%d", filei++);//将储存第filei组内排序后的数据的文件的文件名命名为filei
FILE* fin = fopen(subfile, "w");//创建一个以字符串subfile[20]为名字的文件并打开
if (fin == NULL)
{
printf("打开文件失败\n");
exit(-1);
}
//将内排序排好的数据写入到subfile文件中
for (int i = 0; i < n; i++)
{
fprintf(fin, "%d\n", a[i]);
}
fclose(fin);//关闭subfile文件
i = 0;
memset(a, 0, sizeof(int)*n);//将a数组内存置0,准备再次读取10个数据进行内排序
}
}
// 利用互相归并到文件,实现整体有序
char mfile[100] = "12";//归并后文件的文件名
char file1[100] = "1";//待归并的第一个文件的文件名
char file2[100] = "2";//待归并的第二个文件的文件名
for (int i = 2; i <= n; ++i)
{
//将file1文件和file2文件中的数据归并到mfile文件中
_MergeFile(file1, file2, mfile);
strcpy(file1, mfile);//下一次待归并的第一个文件就是上一次归并好的文件
sprintf(file2, "%d", i + 1);//上一次待归并的第二个文件的文件名加一,就是下一次待归并的第二个文件的文件名
sprintf(mfile, "%s%d", mfile, i + 1);//下一次归并后文件的文件名
}
fclose(fout);//关闭文件
}
//主函数
int main()
{
MergeSortFile("Sort.txt");//将Sort.txt文件中的数据进行排序
return 0;
}
注:代码中使用的是第二种文件合并的方式。
计数排序
计数排序,又叫非比较排序。顾名思义,该算法不是通过比较数据的大小来进行排序的,而是通过统计数组中相同元素出现的次数,然后通过统计的结果将序列回收到原来的序列中。
举个例子:
上列中的映射方法称为绝对映射,即arr数组中的元素是几就在count数组中下标为几的位置++,但这样会造成空间浪费。例如,我们要将数组:1020,1021,1018,进行排序,难道我们要开辟1022个整型空间吗?
若是使用计数排序,我们应该使用相对映射,简单来说,数组中的最小值就相对于count数组中的0下标,数组中的最大值就相对于count数组中的最后一个下标。这样,对于数组:1020,1021,1018,我们就只需要开辟用于储存4个整型的空间大小了,此时count数组中下标为i的位置记录的实际上是1018+i这个数出现的次数。
总结:
绝对映射:count数组中下标为i的位置记录的是arr数组中数字i出现的次数。
相对映射:count数组中下标为i的位置记录的是arr数组中数字min+i出现的次数。
注:计数排序只适用于数据范围较集中的序列的排序,若待排序列的数据较分散,则会造成空间浪费,并且计数排序只适用于整型排序,不适用与浮点型排序。
代码:
cpp
//计数排序
void CountSort(int* a, int n)
{
int min = a[0];//记录数组中的最小值
int max = a[0];//记录数组中的最大值
for (int i = 0; i < n; i++)
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;//min和max之间的自然数个数(包括min和max本身)
int* count = (int*)calloc(range, sizeof(int));//开辟可储存range个整型的内存空间,并将内存空间置0
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//统计相同元素出现次数(相对映射)
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int i = 0;
//根据统计结果将序列回收到原来的序列中
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);//释放空间
}