一道面试题:4G内存, 100G文件,文件中存放字符串,排序这100G文件字符串数据
内部排序:
内存中可以存放所有待排序的数据,可以使用高效的内部排序来完成例如快速排序,归并排序,堆排序等。
外部排序:
待排序数据过大,内存不能一次全部加载来完成排序。一般采用分段加载内部排序,然后多路合并,产生最后排序结果。
1. 基本实现方式:
将100G文件分成100份排号为1,2...100,每份1G加载进内存,然后对每份文件采用内部排序来排序,字符串应该有高效的内部排序方式,将排序结果写入到100份文件中,每份文件都是有序的;
将内存分成3份 1G 1G 2G ,从文件1加载到内存中,从文件2数据到内存中,将这两个文件数据进行归并排序,最终结果写到一个2G文件中,这样依次进行归并排序,可以产生50份2G文件;进行进行归并直到整个文件有序。
存在的问题: 文件大小的划分,归并排序可以采用多路归并排序。
大神的一个例子,假设有一个含10000 个记录的文件,首先通过10 次内部排序得到10 个初始归并段R1~R10 ,其中每一段都含1000 个记录。然后对它们作两两归并,直至得到一个有序文件为止:
2.多路归并方式:
普通两路归并方式,每次归并两个文件,即从两个文件中选取较小者放入到目标文件中,可以采用K路归并方式,即每一次归并K个文件,即从K个文件中选取较小者放入到目标文件中。增大k可以减少外存信息读写时间,但k个归并段中选取最小的记录需要比较k-1次,为得到u个记录的一个有序段共需要(u-1)(k-1)次,若归并趟数为s次,那么对n个记录的文件进行外排时,内部归并过程中进行的总的比较次数为s(n-1)(k-1)。若共有m个归并段,则s=logkm,所以总的比较次数为: (logkm)(k-1)(n-1)=(log2m/log2k)(k-1)(n-1),而(k-1)/log2k随k增而增因此内部归并时间随k增长而增长了,抵消了外存读写减少的时间,这样做不行,由此引出了“败者树”tree of loser的使用。在内部归并过程中利用败者树将k个归并段中选取最小记录比较的次数降为(log2k)次,使总比较次数为(log2m)(n-1)与k无关。
败者树是完全二叉树,可以采用一维数组。其元素个数为k个叶子结点、k-1个比较结点、1个冠军结点共2k个。ls[0]为冠军结点,ls[1]--ls[k-1]为比较结点,ls[k]--ls[2k-1]为叶子结点(同时用另外一个指针索引b[0]--b[k-1]指向)。另外bk为一个附加的辅助空间,不属于败者树,初始化时存着MINKEY的值。
多路归并排序算法的过程大致为:
1):首先将k个归并段中的首元素关键字依次存入b[0]--b[k-1]的叶子结点空间里,然后创建败者树,创建完毕之后最小的关键字下标(即所在归并段的序号)便被存入ls[0]中。然后不断循环:
2)把ls[0]所存最小关键字来自于哪个归并段的序号得到为q,将该归并段的首元素输出到有序归并段里,然后把下一个元素关键字放入上一个元素本来所在的叶子结点b[q]中,调用Adjust顺着b[q]这个叶子结点往上调整败者树直到新的最小的关键字被选出来,其下标同样存在ls[0]中。循环这个操作过程直至所有元素被写到有序归并段里。
以4路归并排序为例,叶子结点存放每个归并段的最小值,然后依次进行比较,失败者成为根节点,成功者进行下一轮比较,5,7 7失败成为根节点,5成功 29,9 29失败为根节点 9成功 5,9下一轮比较 5成功写入输出缓冲区,9失败成为根节点
由于5最小,已经输出,因此把输出元素所在的归并路加载下一个元素,再依次进行比较,有点类似于堆排序,每次将最小的元素输出。
编程实现k路归并排序如下:
每个归并段声明一个对象,保存数组 数组大小以及当前归并的元素序号
class Arr{
//归并段的数组
int[] arr;
//归并段总长度
int num;
//归并段的指针
int pos;
public Arr(int[] arr, int num, int pos){
this.arr = arr;
this.num = num;
this.pos = pos;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Arr [arr=");
builder.append(Arrays.toString(arr));
builder.append(", num=");
builder.append(num);
builder.append(", pos=");
builder.append(pos);
builder.append("]");
return builder.toString();
}
}
完整代码如下:
public class KMergeSort {
//最大归并段长
private int LEN = 10;
private int MINKEY = -1;
private int MAXKEY = Integer.MAX_VALUE;
// 败者树数组 存放非叶子节点的归并路索引
private int[] loserTree;
//叶子结点数组 存放叶子节点数据
private int[] external;
private int k;
private List<Arr> numArr;
private int count;
public KMergeSort(Integer k, List<Arr> numArr){
this.k = k;
this.numArr = numArr;
this.loserTree = new int[k];
this.external = new int[k+1];
for(Arr arr : numArr){
this.count = this.count + arr.num;
}
}
private void createLoserTree(){
external[k] = MINKEY;
for(int i=0; i<k; i++){
loserTree[i] = k;
}
//System.out.println("loserTree = " + Arrays.toString(loserTree));
for(int i=k-1; i>=0; i--){
adjust(i);
}
//System.out.println("loserTree = " + Arrays.toString(loserTree));
}
//由某一个节点i出发,向根方向遍历,将最小值往根方向推送
private void adjust(int i) {
int t = (i+k)/2;
while(t>0){
if(external[i]>external[loserTree[t]]){
int tmp = i;
i = loserTree[t];
loserTree[t] = tmp;
}
t = t / 2;
}
loserTree[0] = i;
}
public void KMerge(){
//将每个待归并段第一个元素放入external中,pos指针加一
for(int i=0; i<k; i++){
//System.out.println("numArr = " + numArr.get(i));
int p = numArr.get(i).pos;
external[i] = numArr.get(i).arr[p];
numArr.get(i).pos++;
}
//System.out.println("external = " + Arrays.toString(external));
createLoserTree();
int no = 0;
while(no < count){
int p = loserTree[0];
System.out.println(external[p]);
no++;
if(numArr.get(p).pos>=numArr.get(p).num){
external[p] = MAXKEY;
}else{
external[p] = numArr.get(p).arr[numArr.get(p).pos];
numArr.get(p).pos++;
}
adjust(p);
}
}
public static void main(String[] args) {
//输入K个数组
int k = 8;
List<Arr> numArr = new ArrayList<>();
numArr.add(new Arr(new int[]{1,41,81}, 3, 0));
numArr.add(new Arr(new int[]{2,51,91}, 3, 0));
numArr.add(new Arr(new int[]{4,61,101}, 3, 0));
numArr.add(new Arr(new int[]{5,42,82}, 3, 0));
numArr.add(new Arr(new int[]{6,52,92}, 3, 0));
numArr.add(new Arr(new int[]{7,62,103}, 3, 0));
numArr.add(new Arr(new int[]{8,43,84}, 3, 0));
numArr.add(new Arr(new int[]{9,55,95}, 3, 0));
//System.out.println(numArr);
KMergeSort km = new KMergeSort(k, numArr);
km.KMerge();
}
}
首先构建一颗败者树,从非叶子结点出发,将最小值放置于树根位置;输出树根位置元素,提取输出元素所在的归并段的下一个元素到external数组中,然后从该位置向根方向比较交换,将最小值置于树根位置,依次输出,直到所有归并段都已为空。
使用败者树类似于使用最小堆,可以维持一个最小堆来实现k个数取最小值,初次建堆的复杂度为klogk,之后每一次进行输出加入新元素调整复杂度为logk,假设一共输出N个元素,则有 klogk+Nlogk。
参考链接:
https://www.cnblogs.com/codeMedita/p/7425291.html
http://www.cnblogs.com/HappyXie/archive/2012/08/29/2662624.html
https://blog.csdn.net/u010367506/article/details/23565421/