外部排序使用场景及来源
主要针对大容量数据进行排序
在使用选择排序,插入排序,冒泡排序,和快速排序时的最大时间复杂度是O(n^2),因此对于几十万的数据量时排序要耗费很长的时间。对于外部的文件进行数据排序,首先要将数据送入内存中,然后对他们进行内部,但是如果文件过大,那么文件的所有数据不能送入内存,因此就有了外部排序。
外部排序的思路
- 首先将数据从文件中分段读入内存,并使用内部排序算法对于分段的数组进行排序,然后将数组输出到一个临时文件中。得到一个分段排序好的文件,类似下面这样
分为四段有序的数据段 - 将数据段 进行归并到一个大一些的有序分段中,并将新分段存储到新的临时文件中。然后循环该操作。
算法的思路流程图如下
a. 生成800000个随机数据, 分为8段 ,每段存储100000个数据。
b.每段的数据使用数组自带的排序算法进行排序即 Arrays.sort(list,0,i);
c.将s0,s1,s2,s3,s4,s5,s6,s7这八段数据进行两两归并排序。s0和s4归并,s1和s5归并,s2和s6归并,s3和s7归并 成为四个有序的段,依次重复归并直到总段数为1
主要算法代码实现
- 生成800000个随机数据 写到largedata.dat文件里面
DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("largedata.dat")));
for(int i=0;i<800000;i++){
int number = (int) (Math.random()*100000);
output.writeInt(number);
}
- 将两个段进行合并 并写到新的段里面 可以理解为合并两个数组,只不过这两个数组存储在文件里面
/**
* 参数1:segmentSize 段的大小 即100000
* 参数2,3是比较的数据文件
* 参数4 存储新的有序数据文件
*/
private static void mergeTwoSegments(int segmentSize,DataInputStream f1,DataInputStream f2,DataOutputStream f3) throws Exception{
int intFormF1 = f1.readInt();
int intFormF2 = f2.readInt();
int f1count =1;
int f2count =1;
//将f1.f2 有序写进f3中
while(true){
if(intFormF1<intFormF2){
f3.writeInt(intFormF1);
if(f1.available()==0||f1count++>segmentSize){
//读到最后一个的时候发现f1为空此时直接将f2加进f3中
f3.writeInt(intFormF2);
break;
}else{
intFormF1 = f1.readInt();
}
}else{
f3.writeInt(intFormF2);
if(f2.available()==0||f2count++>segmentSize){
//读到最后一个的时候发现f2为空此时直接将f1加进f3中
f3.writeInt(intFormF1);
break;
}else{
intFormF2 = f2.readInt();
}
}
}
while(f1.available()>0&&f1count++<segmentSize){
f3.writeInt(f1.readInt());
}
while(f2.available()>0&&f2count++<segmentSize){
f3.writeInt(f2.readInt());
}
}
- 合并所有的分段
该函数的主要目的是
如果numberOfSegments=8 表示有8个段执行循环归并成4个段 ,
如果unmberOfSegments=4表示有4个段执行循环归并之后为2个段。
函数执行的结果就是合并所有的分段并将新形成的分段存储在f3中。
/***
* 参数1 表示段的个数
* 参数2 段的大小
* 参数3 原始段,
* 参数4,原始段的一半。因为对于文件的读写来说符合先进后出的特性,因此从原始段中读取一半赋值个f2,此时原始段中的读的指针就剩未读的数据了。因此可以归并比较 示意图如下
*/
private static void mergeSegments(int numberOfSegments,
int segmentSize,DataInputStream f1,
DataInputStream f2,DataOutputStream f3) throws Exception{
for(int i=0;i<numberOfSegments;i++){
mergeTwoSegments(segmentSize,f1,f2,f3);
}
while(f1.available()>0){
f3.writeInt(f1.readInt());
}
}
- 将f1的一半复制给f2 ,并且进行合并操作
private static void copyHalfToF2(int numberOfSegments,int segmentSize,
DataInputStream f1,DataOutputStream f2) throws Exception{
for(int i=0;i<(numberOfSegments/2)*segmentSize;i++){
f2.writeInt(f1.readInt());
}//文件的输入输出符合栈的特点 先进后出
}
public static void mergeOneStep(int numberOfSegments,int segmentSize,String f1,String f2,String f3) throws Exception{
DataInputStream f1input = new DataInputStream(new BufferedInputStream(new FileInputStream(f1),BUFFER_SIZE));
DataOutputStream f2output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f2),BUFFER_SIZE));
copyHalfToF2(numberOfSegments,segmentSize,f1input,f2output);
f2output.close();
//合并f1中剩下的和 f2段 写到f3中
DataInputStream f2input = new DataInputStream(new BufferedInputStream(new FileInputStream(f2),BUFFER_SIZE));
DataOutputStream f3output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f3),BUFFER_SIZE));
//此时f1的读指针没有发生移动
mergeSegments(numberOfSegments/2,segmentSize,f1input,f2input,f3output);
f1input.close();
f2input.close();
f3output.close();
}
- 递归归并
public static void merge(int numberOfSegments,int segmentSize,String f1,String f2,String f3,String target) throws Exception{
if(numberOfSegments>1){
mergeOneStep(numberOfSegments,segmentSize,f1,f2,f3);
merge((numberOfSegments+1)/2,segmentSize*2,f3,f1,f2,target);
}else{
File file = new File(target);
if(file.exists()) file.delete();
new File(f1).renameTo(file);
}
}
这个递归有点难以理解,
因为mergeOneStep 调用了copyHalfToF2 和mergeSegments,mergeSegments 又调用了mergeTwoSegment,嵌套的有点深。
总结主要函数的功能。
merge
:负责合并 减少段的个数,增大段的大小。以容纳数据
mergeOneStep
负责将数据文件分成两大段,并且将两大段分别归并成新的段。
mergeSegments
将两大新段里面的子段分别合并。被mergeOneStep调用。