大数据排序(10亿量级以上)C语言实现
我们平常对数据进行排序一般用内部方法,即八大排序方法:
- 直接插入排序
- 冒泡排序
- 希尔排序
- 堆排序
- 归并排序
- 堆排序
- 快速排序
- 基数排序
这些排序方法默认你们已经掌握了,如果不了解可以在网上搜一下
首先给出设计的大纲,一共分三步:
- 先生成10亿随机数数据
- 将10亿数据分成n个小文件并进行排序
- 最后将n个小文件进行归并
这里可能大家就会有疑问了,为什么要分好几个小文件呢?
这是由于我们的堆栈无法一次性地存入10亿个数据,因此我们要进行外部排序,即分成n个小文件,然后在进行归并合到目标文件中,这样达到排序目的
主函数代码如下:
#include <iostream>
#include<time.h>
#include"random.h"
#include"divid.h"
#include"result.h"
using namespace std;
int main()
{
unsigned int begin, end,begin1,begin2,begin3,end1,end2,end3;
begin = (unsigned int)time(NULL);
begin1 = (unsigned int)time(NULL);
random();
begin2 = (unsigned int)time(NULL);
end1 = (unsigned int)time(NULL);
std::cout << "创建文件用时" << (end1 - begin1)<<"s"<<endl;
divid();
begin3 = (unsigned int)time(NULL);
end2 = (unsigned int)time(NULL);
std::cout << "第一次排序用时" << (end2 - begin2) << "s"<<endl;
result();
end3 = (unsigned int)time(NULL);
end = (unsigned int)time(NULL);
std::cout << "第二次排序加写入文件用时" << (end3 - begin3) << "s"<<endl;
std::cout << "总用时为" << (end - begin)<<"s"<<endl;
}
接下来我们要生成随机数,我才用的是scrand函数将当前时间作为种子来随机生成无符号整型数,代码如下(random.h):
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int random()
{
FILE* fp;
fopen_s(&fp, "D:\\算法\\10亿数排序\\main\\data2.txt", "w");
if (fp == NULL)
{
return 0;
}
long int length = 1000000000;
unsigned long int a;
srand((unsigned long int)time(NULL));
for (int i = 0; i < length; i++)
{
a = rand();
fprintf(fp, "%d ", a);
if (i%30==0&&i!=0)
{
fprintf(fp, "\n");
}
}
fclose(fp);
return 0;
}
接下来就到我们的重点了,如何将数据分成n个小文件进行排序,这里我采用的是快速排序,将数据分成500个有序的数据文件这个就不赘述了很简单理解,代码如下(divid.h):
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include"QuickSort.h"
#define MAX 1000000000/500
#define MAX2 500
int divid()
{
char name[80];
double begin,begin1;
double end,end1;
FILE* fp;
int *a;
a = new int[MAX+1];
int i;
int j;
fopen_s(&fp, "D:\\算法\\10亿数排序\\main\\data2.txt", "r");
if (fp == NULL)
{
return 0;
}
std::cout.precision(3);
begin1 = (double)time(NULL);
for (j = 1; j <= MAX2; j++)
{
sprintf_s(name, "D:\\算法\\10亿数排序\\main\\divideandsort\\%02d.txt", j);
//读取文件
begin = (double)time(NULL);
for (i = 1; i < MAX + 1; i++)
{
fscanf_s(fp, "%d", &a[i]);
}
end = (double)time(NULL);
//std::cout << "读入数据花费的时间:" << (end - begin) << "\n";
//快速排序
begin = (double)time(NULL);
QuickSort(a,MAX);
end = (double)time(NULL);
//std::cout << "快速排序所花费的时间:" << (end - begin) << "\n";
//写入文件
FILE* fp1;
fopen_s(&fp1, name, "w");
begin = (double)time(NULL);
if (fp1 == NULL)
{
return 0;
}
for (i = 1; i < MAX; i++)
{
fprintf_s(fp1, "%d ", a[i]);
if (i % 30 == 0 && i != 0)
{
fprintf(fp1, "\n");
}
}
fprintf_s(fp1, "%d", a[i]);
end = (double)time(NULL);
fclose(fp1);
//std::cout << "写入数据花费的时间:" << (end - begin) << "\n";
}
end1 = (double)time(NULL);
//std::cout << "将十亿数分成十六个文件并排序所花费的时间:" << (end1 - begin1);
fclose(fp);
delete[] a;
return 0;
}
下面是我自己写的快速排序模板,如果你想用库函数也行。
#pragma once
template <class T>
int Partition(T L[], int low, int high)//一次快排
{
int pivotkey;
L[0] = L[low];
pivotkey = L[low];
while (low < high)
{
while (low < high && L[high] >= pivotkey)
{
--high;
}
L[low] = L[high];
while (low < high && L[low] <= pivotkey)
{
++low;
}
L[high] = L[low];
}
L[low] = L[0];
return low;
}
template <class T>
void QSort(T L[], int low, int high)//比较函数
{
int pivotloc;
if (low < high)
{
pivotloc = Partition(L, low, high);
QSort(L, low, pivotloc - 1);
QSort(L, pivotloc + 1, high);
}
}
template<class T>
void QuickSort(T L[],int max)//快速排序
{
QSort(L, 1, max);
}
我们这样就完成了外部排序,接下来要做的是如何归并,我们首先得开个500大小的数组存每个文件的第一个数据,然后在开一个500的文件流数组便于读数据。
这里可能会有疑问为什么只能开500个文件流?
因为文件流最大只能同时开508个,因此我们所用的文件流最大只能开到500,因为正好整除,这也是第二次只分500个文件的原因。
接下来就是如何保证每次输入到目标文件里是最小的,我这里有三种方法(result.h):
第一种方案:我们可以每次遍历数组找到最小的然后写到文件里,再上对应文件中读入下一个数据,再遍历,一次类推,代码如下:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include <iostream>
using namespace std;
#define MAX3 500
//最小值所在数组的下标
template <class Type> int Min(Type* a, int N)
{
int m = -1;//作为所以文件都读取完毕的标志
for (int i = 0; i < N; ++i)
{
if (a[i] == -1)//此文件已读取完毕,所以-1不作为一个数参与比较
continue;
if (m == -1 || a[m] > a[i])
m = i;
}
return m;
}
int result()
{
//合并文件
time_t begin, end;
FILE* fp = NULL;
FILE* FileList[MAX3];
errno_t err;
if ((err = fopen_s(&fp, "D:\\算法\\10亿数排序\\main\\result.txt", "w")) != 0)//创建数据输出的文件
{
cout << "File open error!\n";
return 0;
}
for (int i = 0; i < MAX3; i++)
{
char name[200];
sprintf_s(name, "D:\\算法\\10亿数排序\\main\\divideandsort\\%02d.txt", i + 1);
fopen_s(&FileList[i], name, "r");
}
int number[MAX3];//每个文件中最小的元素即第一个元素
for (int i = 0; i < MAX3; i++)
fscanf_s(FileList[i], "%d", &number[i]);
//开始归并
while (1)
{
int min = Min(number, MAX3);
cout << number[min] << ends;
if (min == -1)
break; //所有文件读取完毕
fprintf(fp, "%d ", number[min]);
fscanf_s(FileList[min], "%d", &number[min]);
if (feof(FileList[min]))
{
number[min] = -1; //本文件读取完毕
}
}
for (int i = 0; i < MAX3; i++)
fclose(FileList[i]);
fclose(fp);
}
运行结果如下:
我们发现其实并不快,那么是为什么影响其速度呢,那就是每次都得做n次比较,因此比较浪费空间,因此想到了第二种方法。
第二种方案:既然比较浪费了这么多时间做比较,我们可以第一遍将数组排成有序的每次将数组中的第一个元素写到目标文件中,然后新加的元素做二分插入排序,以此类推,这样最坏是n次比较,最好1次比较,代码如下:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include <iostream>
using namespace std;
#define MAX1 500+1
struct arr
{
int a;
int flag;
};
void Exchange(arr L[], int i, int j)
{
int temp;
temp = L[i].a;
L[i].a = L[j].a;
L[j].a = temp;
temp = L[i].flag;
L[i].flag = L[j].flag;
L[j].flag = temp;
}
int partition(arr l[], int low, int high)//一次快排
{
int pivotkey;
Exchange(l, 0, low);
pivotkey = l[low].a;
while (low < high)
{
while (low < high && l[high].a >= pivotkey)
{
--high;
}
l[low] = l[high];
while (low < high && l[low].a <= pivotkey)
{
++low;
}
Exchange(l, high, low);
}
Exchange(l, low, 0);
return low;
}
void qsort(arr l[], int low, int high)//比较函数
{
int pivotloc;
if (low < high)
{
pivotloc = partition(l, low, high);
qsort(l, low, pivotloc - 1);
qsort(l, pivotloc + 1, high);
}
}
void quicksort(arr l[], int max)//快速排序
{
qsort(l, 1, max);
}
void InsertSort(arr L[],int max)//二分查找法插入排序
{
int i;
for (i=1;i<max-1;i++)
{
if (L[i].a<=L[i+1].a)
{
break;
}
Exchange(L, i, i + 1);
}
}
int result()
{
arr b[MAX1];
int i;
int flag1;
int j;
int k=0;
int tmp;
char name[80];
FILE* fp[MAX1];
FILE* fp1;
fopen_s(&fp1, "D:\\算法\\10亿数排序\\main\\result.txt", "w");
if (fp1 == NULL)
{
return 0;
}
for (i = 1; i < MAX1; i++)
{
sprintf_s(name,"D:\\算法\\10亿数排序\\main\\divideandsort\\%02d.txt", i);
fopen_s(&fp[i], name, "r");
if (fp[i] == NULL)
{
return 0;
}
fscanf_s(fp[i], "%d", &b[i].a);
b[i].flag = i;
}
quicksort(b,MAX1);
flag1 = 1;
j = 1;
while (flag1)
{
flag1 = 0;
fprintf_s(fp1, "%d ", b[1].a);
//cout << b[1].a<<ends;
if (j == 30)
{
fprintf_s(fp1, "\n");
j = 1;
}
else
{
j++;
}
tmp = b[1].flag;
if (tmp > 0)
{
if (!feof(fp[tmp]))
{
fscanf_s(fp[tmp], "%d", &b[1].a);
flag1 = 1;
}
else
{
for (i = 1; i < MAX1; i++)
{
if (fp[i] != NULL)
{
fscanf_s(fp[i], "%d", &b[1].a);
b[1].flag = i;
break;
flag1 = 1;
}
}
if (!flag1)
{
break;
}
}
InsertSort(b,MAX1);
}
}
for (i = 2; i < MAX1; i++)
{
fprintf_s(fp1, "%d ", b[i].a);
//cout << b[1].a<<ends;
}
fclose(fp1);
for ( i = 1; i < MAX1; i++)
{
fclose(fp[i]);
}
return 0;
}
运行结果为:
从运行结果发现性能提高了,但是并未提高太多(这是最好的一次运行结果),那么是为什么导致的呢,这是由于虽然比较少了,但是数据交换次数比较多,这样会占很多的时间和空间,因此笔者做了很多构想,最后想出第三种方案。
第三种方案:那么我们如何将前两种方法综合起来呢,既不做如此多的比较,又使用较少的交换次数,其实这个是最关键的思想,最终我想到了用堆排序的方法,这样500大小的数组最多比较27次,做1次交换,性能大大提高,当我们其中的一个文件读完之后,我们可以将数组头和尾交换,然后数组长度减一,直到数组有效长度为0,代码如下:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include <iostream>
using namespace std;
#define MAX1 (500+1)
struct arr
{
int a;
int flag;
};
//inline void Exchange(arr L[], int i, int j)
//{
// int temp;
// temp = L[i].a;
// L[i].a = L[j].a;
// L[j].a = temp;
// temp = L[i].flag;
// L[i].flag = L[j].flag;
// L[j].flag = temp;
//}
inline void HeepAdjust(arr L[], int s, int m)//堆排序的调整
{
arr rc;
int j;
rc.a = L[s].a;
rc.flag = L[s].flag;
for (j = 2 * s; j < m ; j *= 2)
{
if (j + 1 < m && L[j].a > L[j + 1].a)
{
++j;
}
if (!(rc.a > L[j].a))
{
break;
}
//Exchange(L, s, j);
int temp;
temp = L[s].a;
L[s].a = L[j].a;
L[j].a = temp;
temp = L[s].flag;
L[s].flag = L[j].flag;
L[j].flag = temp;
s = j;
}
L[s].a = rc.a;
L[s].flag = rc.flag;
}
int result()
{
arr b[MAX1];
int i;
int j;
int l;
int tmp;
char name[80];
FILE* fp[MAX1];
FILE* fp1;
fopen_s(&fp1, "D:\\算法\\10亿数排序\\main\\result.txt", "w");
if (fp1 == NULL)
{
return 0;
}
for (i = 1; i < MAX1; i++)
{
sprintf_s(name, "D:\\算法\\10亿数排序\\main\\divideandsort\\%02d.txt", i);
fopen_s(&fp[i], name, "r");
if (fp[i] == NULL)
{
return 0;
}
fscanf_s(fp[i], "%d", &b[i].a);
b[i].flag = i;
}
for (i = MAX1/2;i > 0 ; --i)
{
HeepAdjust(b, i,MAX1);
}
j = 1;
l = MAX1;
while (1)
{
fprintf_s(fp1, "%d ", b[1].a);
//cout << b[1].a << ends;
if (j == 30)
{
fprintf_s(fp1, "\n");
j = 1;
}
else
{
j++;
}
tmp = b[1].flag;
if (tmp > 0)
{
if (!feof(fp[tmp]))
{
fscanf_s(fp[tmp], "%d", &b[1].a);
}
else
{
//Exchange(b, 1, l);
int temp;
temp = b[1].a;
b[1].a = b[l-1].a;
b[l-1].a = temp;
temp = b[1].flag;
b[1].flag = b[l-1].flag;
b[l-1].flag = temp;
l--;
}
if (l<=1)
{
break;
}
HeepAdjust(b, 1, l);
}
}
fclose(fp1);
for (i = 1; i < MAX1; i++)
{
fclose(fp[i]);
}
return 0;
}
运行结果为:
从运行结果可以看出此方法的性能是最好的,综合了前两种方法的特点,既没有比较太多的次数,也没有做太多次的数据交换,所以整体性能最好
至此我们就将10亿随机数排成有序的了。
源代码已上传github:源代码