在 LevelDB 數據庫中高層數據下沉到低層時須要經歷一次 Major Compaction,將高層文件的有序鍵值對和低層文件的多個有序鍵值對進行歸併排序。磁盤多路歸併排序算法的輸入是來自多個磁盤文件的有序鍵值對,在內存中將這些文件的鍵值對進行排序,而後輸出到一到多個新的磁盤文件中。 java
多路歸併排序在大數據領域也是經常使用的算法,經常使用於海量數據排序。當數據量特別大時,這些數據沒法被單個機器內存容納,它須要被切分位多個集合分別由不一樣的機器進行內存排序(map 過程),而後再進行多路歸併算法未來自多個不一樣機器的數據進行排序(reduce 過程),這是流式多路歸併排序,爲何說是流式排序呢,由於數據源來源於網絡套接字。算法
多路歸併排序的優點在於內存消耗極低,它的內存佔用和輸入文件的數量成正比,和數據總量無關,數據總量只會線性正比影響排序的時間。數據庫
下面咱們來親自實現一下磁盤多路歸併算法,爲何是磁盤,由於它的輸入來自磁盤文件。數組
咱們須要在內存裏維護一個有序數組。每一個輸入文件當前最小的元素做爲一個元素放在數組裏。數組按照元素的大小保持排序狀態。 緩存
接下來咱們開始進入循環,循環的邏輯老是從最小的元素下手,在其所在的文件取出下一個元素,和當前數組中的元素進行比較。根據比較結果進行不一樣的處理,這裏咱們使用二分查找算法進行快速比較。注意每一個輸入文件裏面的元素都是有序的。網絡
須要特別注意的是Java 內置了二分查找算法在使用上比較精巧。dom
public class Collections {
...
public static <T> int binarySearch(List<T> list, T key) {
...
if (found) {
return index;
} else {
return -(insertIndex+1);
}
}
...
}
複製代碼
若是 key 能夠在 list 中找到,那就直接返回相應的位置。若是找不到,它會返回負數,還不是簡單的 -1,這個負數指明瞭插入的位置,也就是說在這個位置插入 key,數組將能夠繼續保持有序。ide
好比 binarySearch 返回了 index=-1,那麼 insertIndex 就是 -(index+1),也就是 0,插入點在數組開頭。若是返回了 index=-size-1,那麼 insertIndex 就是 size,是數組末尾。其它負數會插入數組中間。 大數據
對於每個輸入文件都會建立一個 MergeSource 對象,它提供了 hasNext() 和 next() 方法用於判斷和獲取下一個元素。注意輸入文件是有序的,下一個元素就是當前輸入文件最小的元素。 hasNext() 方法負責讀取下一行並緩存在 cachedLine 變量中,調用 next() 方法將 cachedLine 變量轉換成整數並返回。this
class MergeSource implements Closeable {
private BufferedReader reader;
private String cachedLine;
private String filename;
public MergeSource(String filename) {
this.filename = filename;
try {
FileReader fr = new FileReader(filename);
this.reader = new BufferedReader(fr);
} catch (FileNotFoundException e) {
}
}
public boolean hasNext() {
String line;
try {
line = this.reader.readLine();
if (line == null || line.isEmpty()) {
return false;
}
this.cachedLine = line.trim();
return true;
} catch (IOException e) {
}
return false;
}
public int next() {
if (this.cachedLine == null) {
if (!hasNext()) {
throw new IllegalStateException("no content");
}
}
int num = Integer.parseInt(this.cachedLine);
this.cachedLine = null;
return num;
}
@Override
public void close() throws IOException {
this.reader.close();
}
}
複製代碼
在排序前先把這個數組準備好,將每一個輸入文件的最小元素放入數組,並排序。
class Bin implements Comparable<Bin> {
int num;
MergeSource source;
Bin(MergeSource source, int num) {
this.source = source;
this.num = num;
}
@Override
public int compareTo(Bin o) {
return this.num - o.num;
}
}
List<Bin> prepare() {
List<Bin> bins = new ArrayList<>();
for (MergeSource source : sources) {
Bin newBin = new Bin(source, source.next());
bins.add(newBin);
}
Collections.sort(bins);
return bins;
}
複製代碼
關閉輸出文件時注意要先 flush(),避免丟失 PrintWriter 中緩衝的內容。
class MergeOut implements Closeable {
private PrintWriter writer;
public MergeOut(String filename) {
try {
FileOutputStream out = new FileOutputStream(filename);
this.writer = new PrintWriter(out);
} catch (FileNotFoundException e) {
}
}
public void write(Bin bin) {
writer.println(bin.num);
}
@Override
public void close() throws IOException {
writer.flush();
writer.close();
}
}
複製代碼
下面咱們來生成一系列輸入文件,每一個輸入文件中包含一堆隨機整數。一共生成 n 個文件,每一個文件的整數數量在 minEntries 到 minEntries 之間。返回全部輸入文件的文件名列表。
List<String> generateFiles(int n, int minEntries, int maxEntries) {
List<String> files = new ArrayList<>();
for (int i = 0; i < n; i++) {
String filename = "input-" + i + ".txt";
PrintWriter writer;
try {
writer = new PrintWriter(new FileOutputStream(filename));
ThreadLocalRandom rand = ThreadLocalRandom.current();
int entries = rand.nextInt(minEntries, maxEntries);
List<Integer> nums = new ArrayList<>();
for (int k = 0; k < entries; k++) {
int num = rand.nextInt(10000000);
nums.add(num);
}
Collections.sort(nums);
for (int num : nums) {
writer.println(num);
}
writer.flush();
writer.close();
} catch (FileNotFoundException e) {
}
files.add(filename);
}
return files;
}
複製代碼
萬事俱備,只欠東風。將上面的類都準備好以後,排序算法很簡單,代碼量很是少。對照上面算法思路來理解下面的算法就很容易了。
public void sort() {
List<Bin> bins = prepare();
while (true) {
// 取數組中最小的元素
MergeSource current = bins.get(0).source;
if (current.hasNext()) {
// 從輸入文件中取出下一個元素
Bin newBin = new Bin(current, current.next());
// 二分查找,也就是和數組中已有元素進行比較
int index = Collections.binarySearch(bins, newBin);
if (index == 0) {
// 算法思路狀況1
this.out.write(newBin);
} else {
// 算法思路狀況2
if (index < 0) {
index = -(index+1);
}
bins.add(index, newBin);
Bin minBin = bins.remove(0);
this.out.write(minBin);
}
} else {
// 算法思路狀況3:遇到文件尾
Bin minBin = bins.remove(0);
this.out.write(minBin);
if (bins.isEmpty()) {
break;
}
}
}
}
複製代碼
讀者能夠直接將下面的代碼拷貝粘貼到 IDE 中運行。
package leetcode;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class DiskMergeSort implements Closeable {
public static List<String> generateFiles(int n, int minEntries, int maxEntries) {
List<String> files = new ArrayList<>();
for (int i = 0; i < n; i++) {
String filename = "input-" + i + ".txt";
PrintWriter writer;
try {
writer = new PrintWriter(new FileOutputStream(filename));
int entries = ThreadLocalRandom.current().nextInt(minEntries, maxEntries);
List<Integer> nums = new ArrayList<>();
for (int k = 0; k < entries; k++) {
int num = ThreadLocalRandom.current().nextInt(10000000);
nums.add(num);
}
Collections.sort(nums);
for (int num : nums) {
writer.println(num);
}
writer.close();
} catch (FileNotFoundException e) {
}
files.add(filename);
}
return files;
}
private List<MergeSource> sources;
private MergeOut out;
public DiskMergeSort(List<String> files, String outFilename) {
this.sources = new ArrayList<>();
for (String filename : files) {
this.sources.add(new MergeSource(filename));
}
this.out = new MergeOut(outFilename);
}
static class MergeOut implements Closeable {
private PrintWriter writer;
public MergeOut(String filename) {
try {
this.writer = new PrintWriter(new FileOutputStream(filename));
} catch (FileNotFoundException e) {
}
}
public void write(Bin bin) {
writer.println(bin.num);
}
@Override
public void close() throws IOException {
writer.flush();
writer.close();
}
}
static class MergeSource implements Closeable {
private BufferedReader reader;
private String cachedLine;
public MergeSource(String filename) {
try {
FileReader fr = new FileReader(filename);
this.reader = new BufferedReader(fr);
} catch (FileNotFoundException e) {
}
}
public boolean hasNext() {
String line;
try {
line = this.reader.readLine();
if (line == null || line.isEmpty()) {
return false;
}
this.cachedLine = line.trim();
return true;
} catch (IOException e) {
}
return false;
}
public int next() {
if (this.cachedLine == null) {
if (!hasNext()) {
throw new IllegalStateException("no content");
}
}
int num = Integer.parseInt(this.cachedLine);
this.cachedLine = null;
return num;
}
@Override
public void close() throws IOException {
this.reader.close();
}
}
static class Bin implements Comparable<Bin> {
int num;
MergeSource source;
Bin(MergeSource source, int num) {
this.source = source;
this.num = num;
}
@Override
public int compareTo(Bin o) {
return this.num - o.num;
}
}
public List<Bin> prepare() {
List<Bin> bins = new ArrayList<>();
for (MergeSource source : sources) {
Bin newBin = new Bin(source, source.next());
bins.add(newBin);
}
Collections.sort(bins);
return bins;
}
public void sort() {
List<Bin> bins = prepare();
while (true) {
MergeSource current = bins.get(0).source;
if (current.hasNext()) {
Bin newBin = new Bin(current, current.next());
int index = Collections.binarySearch(bins, newBin);
if (index == 0 || index == -1) {
this.out.write(newBin);
if (index == -1) {
throw new IllegalStateException("impossible");
}
} else {
if (index < 0) {
index = -index - 1;
}
bins.add(index, newBin);
Bin minBin = bins.remove(0);
this.out.write(minBin);
}
} else {
Bin minBin = bins.remove(0);
this.out.write(minBin);
if (bins.isEmpty()) {
break;
}
}
}
}
@Override
public void close() throws IOException {
for (MergeSource source : sources) {
source.close();
}
this.out.close();
}
public static void main(String[] args) throws IOException {
List<String> inputs = DiskMergeSort.generateFiles(100, 10000, 20000);
// 運行屢次看算法耗時
for (int i = 0; i < 20; i++) {
DiskMergeSort sorter = new DiskMergeSort(inputs, "output.txt");
long start = System.currentTimeMillis();
sorter.sort();
long duration = System.currentTimeMillis() - start;
System.out.printf("%dms\n", duration);
sorter.close();
}
}
}
複製代碼
本算法還有一個小缺陷,那就是若是輸入文件數量很是多,那麼內存中的數組就會特別大,對數組的插入刪除操做確定會很耗時,這時能夠考慮使用 TreeSet 來代替數組,讀者們能夠自行嘗試一下。