算法15--外部排序

一道面试题: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/

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值