外部排序,是相对于内部排序而言的。之前我分享了很多种排序,这些排序都是将待排序的乱序数组全部放到内存里面,然后执行相应的排序算法,完成排序并输出结果的。整个排序的过程都是在内存里一次性加载所有的待排序数字,然后在内存里完成排序算法,这种叫内部排序。外部排序,就是需要排序的数字太多了,以至于内存一次加载不了所有的数字,然后就只能通过今天分享的外部排序来完成。
外部排序,乱序数字就不是在内存中加载为数组了,而是存在文件上的。Java需要IO读取文件内容,将文件中的数字经过处理,然后IO输出中间文件及最后的排好序的文件,完成整个外部排序过程。本来我打算写代码来完成这个外部排序,但是我觉得写出思路来就可以了,就暂时不写代码了。后面我会给出一些Java的API,用来辅助实现外部排序。
实现思路1、两路合并:
两路合并举例:
待排序文件:2 9 5 3 8 6 1 7 4 0
假设内存一次只能读入4个数字(2 9 5 3 | 8 6 1 7 | 4 0),快速排序后以文件输出,生成3个文件:
文件1:2 3 5 9
文件2:1 6 7 8
文件3:0 4
然后文件两两归并(就是归并排序的那种归并操作方式,两个文件一行行读取内容,然后每次吐出一个数字),较小的输出,然后小的文件接着吐下一个数字,最终两个有序文件归并为一个有序文件:
1和2归并后的有序文件:1 2 3 5 6 7 8 9
3归并后的有序文件:0 4
如果归并后还是1个以上的文件,就接着归并,直到最后只有一个文件:
步骤3中的两个文件归并后的有序文件(外部排序结果):0 1 2 3 4 5 6 7 8 9
最后归并输出的最后一个文件,就是外部排序完成后的有序数字文件了,外部排序完成!
实现思路2、多路合并:
多路合并举例:
待排序文件:8 4 6 2 5 3 9 1 7
假设内存一次只能读入2个数字(8 4 | 6 2 | 5 3 | 9 1 | 7),快速排序后以文件输出,生成5个文件:
文件1:4 8
文件2:2 6
文件3:3 5
文件4:1 9
文件5:7
然后我们N=3,采取3路归并,用优先队列来决定下一个输出的数字和找到哪个文件该继续吐数字,此例的3路归并后,将生成2个文件:
文件1-3归并后的有序文件:2 3 4 5 6 8
文件4-5归并后的有序文件:1 7 9
如果最后不剩下唯一一个文件,则继续重复步骤3。此例中出现了比路数小的文件个数:3<2。此时就需要2路归并了:
步骤3中两个文件归并后的有序文件(外部排序结果):1 2 3 4 5 6 7 8 9
分析:其实两路归并,也是多路归并的一个特例。归并路数可以是2-N,N就是每次读入内存支持的最大数字个数,然后总共需要读的次数。但无论哪种归并,都先要将所有数字读入内存(建议使用快速排序),然后排序,输出N个有序数字文件,这个是跑不掉的,差别就在于归并的套路:
两路归并的话,每次只需要判断两个有序数字文件吐出来的数字谁大谁小就可以了。而多路归并,则需要借助于优先队列,而且还要找到优先队列出队的小数字是哪个文件吐出来的
两路归并的话,生成的中间文件会比较多。而多路归并,生成的中间文件会比较少。比如N=12:
两路归并的中间文件数:6 + 3 + 1 =10
三路归并的中间文件数:4 + 2 = 6
四路归并的中间文件数:3
六路归并的中间文件数:2
归并路数少了,文件IO开销多;归并路数多了,优先队列操作开销多;我们需要根据实际情况,选择合适的归并路数,完成外部排序
附:外部排序可能用到的JavaAPI(假设文件分为很多行,每行有多个数字,以空格分隔):
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* @author LiYang
* @ClassName FileNumberReader
* @Description 从多行文件中,读取固定分隔的数字的工具类
* @date 2019/11/12 15:50
*/
public class FileNumberReader implements Iterator<Long> {
//数据源文件的路径
private String filePath;
//读取是否结束
private boolean isFinished;
//FileReader类,用于读取文件
private FileReader fileReader;
//BufferedReader类,用于逐行读取文件
private BufferedReader bufferedReader;
//读入一行内容,转化的数字字符串List
private List<String> numberList;
//当前读入的数字文件行内容
private String currentLine;
//当前提取的数字的numberList的下标
private int currentIndex;
//数据源文件中数字的分隔符
private String delimiter;
/**
* FileNumberReader类的构造方法
* @param filePath 数据源文件路径
* @param delimiter 文件中数字的分隔符
* @throws FileNotFoundException
*/
public FileNumberReader(String filePath, String delimiter) throws FileNotFoundException {
this.filePath = filePath;
this.isFinished = false;
this.fileReader = new FileReader(filePath);
this.bufferedReader = new BufferedReader(this.fileReader);
this.numberList = new ArrayList<>();
this.delimiter = delimiter;
}
/**
* 根据isFinished,判断是否还有下一个数字
* @return 是否还有下一个数字
*/
@Override
public boolean hasNext() {
return !isFinished;
}
/**
* 返回文件中下一个数字
* @return 下一个数字
*/
@Override
public Long next() {
try {
//如果当前的numberList读完了,则清空numberList
if (currentIndex > numberList.size() - 1){
numberList.clear();
}
//如果numberList被清空或是初始状态
if (numberList.size() == 0) {
//读入一行内容
currentLine = bufferedReader.readLine();
//如果读入的内容为null,证明文件读到末尾了
if (currentLine == null){
//迭代结束
isFinished = true;
//关闭输入流
bufferedReader.close();
fileReader.close();
//返回null,表示该文件已读完
return null;
}
//如果文件没有读完,则将当前的数据行按分隔符弄成numberList
numberList = new ArrayList<>(Arrays.asList(currentLine.split(delimiter)));
//下标置为0,从第一个开始
currentIndex = 0;
}
//返回当前numberList的currentIndex下标的数字
return Long.parseLong(numberList.get(currentIndex++));
} catch (IOException e) {
e.printStackTrace();
}
//出异常返回null
return null;
}
/**
* 测试文件数字读取工具类
* @param args
* @throws FileNotFoundException
*/
public static void main(String[] args) throws FileNotFoundException {
/**
* 数字源文件路径,该文件内容如下:
* 334 6544 255 6711
* 34655 3 512 343
* 6545 63774 34782 82098
* 77 394
*
* 大家可以改为自己的文件路径
*/
String filePath = "C:\\Users\\Administrator\\Desktop\\external.txt";
//该文件数字分隔符为空格,大家可以改为自己的分隔符,注意是正则表达式
String delimiter = " ";
//创建文件数字阅读工具类实例
FileNumberReader fileNumberReader = new FileNumberReader(filePath, delimiter);
//迭代该类实例,通过该类实例的next()方法取到下一个数字
while (fileNumberReader.hasNext()) {
//最后一个输出不是数字,是null,这就证明文件读完了
//可以作为判断的依据
System.out.print(fileNumberReader.next() + " ");
}
}
}
运行FileNumberReader类的main方法,控制台输出如下,测试通过(外部排序可以拿去从文件中读数字了):
334 6544 255 6711 34655 3 512 343 6545 63774 34782 82098 77 394 null
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
/**
* @author LiYang
* @ClassName FileNumberWriter
* @Description 将外部排序的有序数字写入文件的工具类
* @date 2019/11/12 16:16
*/
public class FileNumberWriter {
//写入的文件的路径(包含自定义的文件名)
private String filePath;
//数字分隔符
private String delimiter;
//每行写的数字的个数
private int amountEachLine;
//当前行数字的个数
private int currentLineAmount;
//待写入的文件类
private File file;
//用于写文件的FileWriter类
private FileWriter fileWriter;
//用于写文件的BufferedWriter类
private BufferedWriter bufferedWriter;
/**
* 写数字的文件工具类
* @param filePath 写入的文件的路径(包含自定义的文件名)
* @param delimiter 数字分隔符
* @param amountEachLine 每行写的数字的个数
* @throws IOException
*/
public FileNumberWriter(String filePath, String delimiter, int amountEachLine) throws IOException {
this.filePath = filePath;
this.delimiter = delimiter;
this.amountEachLine = amountEachLine;
this.fileWriter = new FileWriter(filePath);
this.bufferedWriter = new BufferedWriter(fileWriter);
//实例化该文件
this.file = new File(filePath);
//文件不存在,则创建
if (!file.exists()){
file.createNewFile();
}
}
/**
* 向指定文件写入数字(注意,最后一行会多写一个分隔符)
* @param number 准备要写入的数字
* @throws IOException
*/
public void writeNumber(Long number) throws IOException {
//当前行的第几个数字自增
currentLineAmount ++;
//如果已经超过了当前行的最大数量
if (currentLineAmount > amountEachLine){
//换一行
bufferedWriter.newLine();
//重置当前行的数字的第几个
currentLineAmount = 1;
}
//如果已经是当前行的最后一个数字(写了这个数字就要换行了)
if (currentLineAmount == amountEachLine){
//只写入当前数字
bufferedWriter.write(String.valueOf(number));
//如果这个数字写了还不需要换行
} else {
//写入当前数字,以及分隔符
bufferedWriter.write(number + delimiter);
}
}
/**
* 当有序数字写完后,调用该方法,关闭各种输出流
* @throws IOException
*/
public void writeFinish() throws IOException {
bufferedWriter.close();
fileWriter.close();
}
/**
* 测试文件数字写入工具类
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
//写入的文件的路径(包含文件名)
String filePath = "C:\\Users\\Administrator\\Desktop\\write_number.txt";
//为了美观,分隔符为英文逗号和空格
String delimiter = ", ";
//每行写4个数字
int amountEachLine = 4;
//FileNumberWriter工具类实例
FileNumberWriter fileNumberWriter = new FileNumberWriter(filePath, delimiter, amountEachLine);
//随机数类
Random random = new Random();
//写入30个一千万以内的随机整数
for (int i = 1; i <= 30; i++) {
//调用方法,写入数字(注意,最后一行有可能多一个分隔符)
fileNumberWriter.writeNumber((long)random.nextInt(10000000));
}
//写完调用writeFinish()方法,关闭输出流
fileNumberWriter.writeFinish();
}
}
运行FileNumberWriter类的main方法,写入30个一千万以内的随机整数,然后相应路径上有了一个文件,打开文件,内容如下:
6560567, 5469256, 7342852, 5126873
7296214, 469243, 9396550, 5661757
9696285, 3138184, 1499909, 4495839
9281861, 8126570, 9893790, 7408475
5508093, 5307213, 623569, 9570021
5766407, 9533322, 8139950, 4605782
174081, 1960382, 209324, 1638231
9927870, 6185734,
测试通过,可以拿这个去将归并后的有序数字序列写入文件了
import java.util.PriorityQueue;
/**
* @author LiYang
* @ClassName PriorityQueueTest
* @Description 优先队列的JavaAPI应用测试
* @date 2019/11/12 17:03
*/
public class PriorityQueueTest {
/**
* 测试JavaAPI的优先队列PriorityQueue<>类
* @param args
*/
public static void main(String[] args) {
//实例化一个优先队列,保存Long型数据
PriorityQueue<Long> priorityQueue = new PriorityQueue<>();
//调用add()方法,将数字入队
priorityQueue.add(7L);
priorityQueue.add(19L);
priorityQueue.add(13L);
//调用peek()方法,返回优先队列中的最小元素
System.out.println(priorityQueue.peek());
//调用poll()方法,将优先队列中的最小元素出队,并返回
System.out.println(priorityQueue.poll());
//重复上述操作,共计查看和出队三次
//上面的三个数字将从小到大依次出来
//注意,add(),peek(),poll()方法可以随时调用
//如果优先队列的元素全部被poll了,再poll和peek,返回null
System.out.println(priorityQueue.peek());
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.peek());
System.out.println(priorityQueue.poll());
//再往下,就是null了
System.out.println(priorityQueue.peek());
System.out.println(priorityQueue.poll());
}
}
运行PriorityQueueTest类的main方法,控制台输出如下,测试结果符合预期:
7
7
13
13
19
19
null
null
好了,有了上面的思路,以及功能性关键代码,欢快地去搞外部排序吧……