k路最佳归并,对n个初始归并并进行k路平衡归并

  k路最佳归并,对n个初始归并并进行k路平衡归并

  引入:其实K路归并排序很有用。最简单的就是,假设你要对海量数据进行排序,比如TB级的数据(姑且说是TB级的搜索关键词),而我们的内存只有GB级。我们不能一次加载所有数据并对它们进行排序,但我们确实希望最终得到结果。那么我们该怎么办呢?k路合并排序登场。其实这是一种“分而治之”的思想。既然要给Q数排序,就不能一次全部排序。这时我们把Q分成K组,每组有N个数,(kn)并假设这里N个数据的排序在我们记忆的容差范围内。首先,我们分别对每个组进行排序,这样我们就得到了K个有序数组(假设升序)。

  分析:

  (1)如何合并K个排序数组?

  因为我们之前讨论过堆,很明显堆的排序效率是很高的,所以我们很自然的考虑用堆来实现。因为我们想按升序对它们进行排序,所以我们创建了一个最小堆。因为最终排序结果的个数总是前小后大,我们考虑先把所有N个数组的第一个元素(最小个数)放入最小堆,所以最小堆大小是k,如果我们这样调整堆结构,那么它的第一个元素就是min (min (array1),min (array2).min (arrayn)),这显然是所有数字中最小的元素。

  (2)因为它在任何数组中都是按升序排列的,所以一旦我们从堆中删除了最小的元素,就必须找到一个元素来填补这个洞。为此,我们需要找到数组中被删除元素所在的下一个元素,并将其填充到堆中。(有点像这样。全国最精锐的士兵都被抽到了精锐团去打仗,所以每个团最强的都被抽了出来。如果这个人不幸阵亡,从同团中找一个下一个最好的继续顶这个精英团,这样精英团永远有最高的战斗力。)那么,如何根据被删除的堆元素找到被删除的堆元素所在的数组呢?这要求我们创建一个新的复合类型,它既包括当前值,也包括当前值所在数组的id。

  (3)对于每个排序后的数组,随着最小元素的丢失,其未参与排序的值也在逐渐减少,所以我们必须维护一个长度为k的数组,它在每个未参与排序的数组中保持当前位置。一旦这个数组中最小的剩余元素被添加到堆中,那么这个当前位置必须向后移动。

  (4)随着每个数组的当前位置向后移动,最终会到达这个数组的末尾。此时,这个数组不能提供更多的数字。(这很正常,比如军队里有一个尖刀连,里面包含了最优秀的人,所以最后挑选精英团的时候,总是从这个连里挑选,然后这个连最后肯定是冷清的。)所以我们无法从当前删除的数字所在的数组中找到下一个值。此时,我们必须选择下一个带有值的数组id,并挑选出它的最小值。该方法为ArrayIDFordeletedData=(ArrayIDFordeletedData 1)% k。

  (5)最后总是所有数组位置都在末尾,也就是所有数组都不能提供不参与排序的值,所以这个时候我们要判断当前堆是否为空。如果不是,那么它们包含了n * k中最大的数,我们依次删除Min(),按最小的顺序输出最大的数。如果当前堆为空,直接跳出循环。

  因此,最终的时间复杂度仅为O(n*logk)

  代码:

  记住上面的关键技术细节,这里的代码很容易编写。

  首先,我们定义一个value对象,它封装了一个整数,以及这个整数来自哪个数组。

  package com . Charles . algo . kway merge;

  /**

  *

  *这个对象表示它是一个可以跟踪它来自哪个数组的数据对象。

  * @作者charles.wang

  *

  */

  公共类TrackableData {

  //数据表示具体值。

  私有int数据;

  //comefromrarray指示该值来自哪个数组。

  private int comeFromArray

  public TrackableData(int data,int comeFromArray){

  this.data=data

  this . come from array=come from array;

  }

  public int getData() {

  返回数据;

  }

  public void setData(int data) {

  this.data=data

  }

  public int getcomefromrarray(){

  返回comeFromArray

  }

  public void setcomefroarray(int comefroarray){

  this . come from array=come from array;

  }

  }然后我们定义一个最小堆,这是解决问题的关键。需要注意的是,它包含的元素应该是上述的值对象。进入堆的时候,调整堆,基于堆的计算,都是值对象的数据字段。

  package com . Charles . algo . kway merge;

  /**

  * @作者charles.wang

  *

  */

  公共类MinHeap {

  //最小堆的存储是一个数组,为了计算,我们不把内容放在第一个位置。

  私有TrackableData[]数据;

  //堆的大小

  private int heapSize

  //当前元素的数量

  private int currentSize

  public MinHeap(int maxSize) {

  heapSize=maxSize

  //创建比最大数多1的数组的作用是启用数组的header元素。为了操作方便,从1开始的运算更容易计算。

  data=new trackable data[heap size 1];

  currentSize=0;

  }

  /**

  *返回当前堆是否为空。

  * @返回

  */

  public boolean isEmpty(){

  if(currentSize==0)

  返回true

  返回false

  }

  /**

  *这里我们检查堆的插入。因为最小堆内部结构中数组前面的元素总是按照最小堆来构建的,所以我们总是从尾部插入。解决方案是:步骤

  * 1:首先将当前元素插入数组的末尾。步骤2:递归比较当前元素和父节点元素。步骤

  * 3:如果当前元素小于父节点的元素,则向上移动当前元素,直到无法移动为止。

  *

  * @param值

  * @返回

  */

  public Minh EAP insert(trackable data值){

  //首先判断堆是否已满,如果是,则不能插入。

  if (currentSize==heapSize)

  还这个;

  //如果堆未满,则堆中仍有可插入的位置。让我们找到最后插入的地方。

  //currentPos指示要插入的当前位置的数组下标。

  int current pos=currentSize 1;

  //先插入到当前位置,因为从1开始,所以数组下标操作也需要1。

  data[current pos]=value;

  //然后将当前元素与其父元素进行比较

  //当前元素是data[currentPos],父元素是data[(currentPos/2),一直遍历到根。

  TrackableData temp

  //如果currentPos为1,表示是插入堆中的第一个元素,则不需要比较。

  //否则,如果插入了多个元素,则将插入位置的元素与其父元素进行比较。

  while (currentPos 1) {

  //如果当前元素小于父元素,则交换它们的位置。

  if (data[currentPos].getData() data[currentPos/2]。getData()) {

  temp=data[current pos/2];

  data[current pos/2]=data[current pos];

  data[current pos]=temp;

  //更新当前位置

  current pos=current pos/2;

  }

  //否则,假设现有堆是最小堆,说明现在插入的位置是正确的,不需要转换。

  否则{

  打破;

  }

  }

  //插入后,将当前堆中的元素数加1。

  currentSize

  还这个;

  }

  /**

  *这里因为删除堆是最小堆,所以可以肯定删除最小堆就是删除堆的根元素。此外,必须调整剩余堆以保持最小堆。

  *因为删除最小元素后最小元素位置有空位,所以解决方法是:步骤1:将堆中最后一个元素复制到这个空位步骤

  * 2:依次比较这最后一个元素的值,当前位置的左右子元素的值,从而降低到合适的位置步骤3:从堆数组中移除最后一个元素。

  */

  public trackable data delete min(){

  //如果最小堆已经为空,则不能删除最小元素。

  if (currentSize==0)

  返回null

  //否则堆不为空,那么最小的元素总是堆中的第一个元素

  trackable data minValue=data[1];

  //由于最小的元素已被删除,堆中currentSize的大小将为-1。因此,我们必须为数组中的最后一个元素找到一个合适的新位置。

  //堆中的最后一个元素

  trackable data last value=data[currentSize];

  //首先将堆中的最后一个元素移动到最小堆的头部

  data[1]=last value;

  //将堆内部存储数组的最后一个元素清零

  data[currentSize]=null;

  //并且当前堆的大小应该是-1

  currentSize-;

  //现在开始调整堆结构,让它还是一个最小堆。

  int current pos=1;//当前位置设置为根,从根开始比较左右两边

  int left pos=current pos * 2;

  TrackableData leftValue

  TrackableData rightValue

  TrackableData temp

  //如果左侧位置与当前堆的总容量相同,则只有2个元素,一个是根元素,一个是根的左侧元素。

  if (leftPos==currentSize) {

  //此时,如果根左元素数据[2]小于根元素数据[1],则交换两个元素的位置。

  if(数据[2]。getData() data[1]。getData()) {

  temp=data[2];

  数据[2]=数据[1];

  data[1]=temp;

  }

  }

  否则{

  //保持循环的条件是节点的左边位置小于当前堆中的元素个数,那么节点必须有右边的子元素,并且位置是左边的子元素位置1。

  while (leftPos currentSize) {

  //获取当前位置左侧子节点的值

  left value=data[left pos];

  //获取当前周期中该位置的右子节点的值。

  right value=data[left pos 1];

  //如果当前值小于左右两个子节点,则当前值位置是正确的。

  if (data[currentPos].getData() leftValue.getData()

  数据[当前位置]。get data()right value . get data()){

  打破;

  }

  //否则,将左侧子节点与右侧子节点进行比较

  //如果左子节点比右子节点小(当然也比当前节点小),那么左子节点和当前节点交换位置。

  else if(left value . get data()right value . get data()){

  temp=data[current pos];

  data[current pos]=left value;

  data[left pos]=temp;

  //同时更新当前位置是左子节点的位置,新的左子节点的位置是左子节点的左子节点。

  currentPos=leftPos

  left pos=current pos * 2;

  }

  //如果右子节点比左子节点小(当然也比当前节点小),那么右子节点和当前节点交换位置。

  否则{

  temp=data[current pos];

  data[current pos]=right value;

  data[left pos 1]=temp;

  //同时更新当前位置是右子节点的位置,新的左子节点是右子节点的左子节点。

  current pos=left pos 1;

  left pos=current pos * 2;

  }

  }

  }

  返回minValue

  }

  }最后,让我们实现K路合并器,这是相当容易实现的,但当涉及到一些下标操作时,我们必须特别小心。因为我们想通用,K和N都是导入的。事实上,如果我们预先计划好K和N,我们根本不需要在内部维护这些数字,因为我们只需要将它们存储在最小堆中。

  package com . Charles . algo . kway merge;

  导入Java . util . ArrayList;

  导入Java . util . list;

  /**

  *

  *这个类用于演示K路合并。

  *

  * @作者charles.wang

  *

  */

  公共类KWayMerger {

  二等兵KWayMerger() {

  }

  /**

  * k路合并,这里的指导思想如下:

  *

  * (1)首先构造一个最小堆,堆中元素的初始值是每个数组中最小的元素。

  * (2)每次打印并删除最小堆中的最小元素,然后将这个最小元素所在数组中的下一个元素插入到最小堆中。(3)在每(2)个结束后,调整堆以保持这个最小的堆。

  */

  public static void mergeKWay(int k,int n,Listint[] arrays) {

  //每个数组的所有当前下标都存储在这里,在开始插入之前,每个数组的当前下标都设置为0。

  int[]index inarrays=new int[k];

  for(int I=0;我k;i ) {

  index inarrays[I]=0;

  }

  //首先构造一个最小堆,大小为k。

  Minh EAP Minh EAP=new Minh EAP(k);

  //第一步,将每个数组的第一个元素依次插入到最小堆中。

  //然后将所有数组的下标指向1

  for(int I=0;我k;i ) {

  //这里每个构造一个TrackableData对象:

  //其中:arrays.get(i)[0]表示其值是第I个数组下标为0的元素(即第I个数组的第一个元素)

  //i表示这个对象来自第I个数组。

  minheap . insert(new trackable data(arrays . get(I)[0],I));

  indexInArrays[I]=1;

  }

  //第二步,反复插入和删除最小堆。

  trackable data currentDeletedData;

  trackable data currentinsertddata;

  int arrayIdForDeletedData

  int nextValueIndexInArray

  //循环的条件是K个数组中至少有一个数组的值没有插入到minHeap中。

  while (true) {

  //此变量维护有多少数组的当前下标越界,即数组的所有元素都已被完全处理。

  int noOfArraysThatCompletelyHandled=0;

  //是用所有数组的当前下标来查询和维护数组。如果它们出界了,就说明它们已经被比较过了。

  for(int I=0;我k;i ) {

  if (indexInArrays[i]==n)

  noofarraysthatcompletely handled;

  }

  //如果已经比较了所有数组中的所有值,那么检查堆中的内容是否为空。

  if(noOfArraysThatCompletelyHandled==k){

  而(!minHeap.isEmpty()) {

  currentDeletedData=min heap . delete min();

  //打印出当前号码

  system . out . print(currentdeleteddata . get data() );

  }

  打破;

  }

  currentDeletedData=min heap . delete min();

  //打印出当前号码

  system . out . print(currentdeleteddata . get data() );

  //获取当前删除的数字来自哪个数组。

  arrayIdForDeletedData=currentdeleteddata . getcomefromrarray();

  //获取该数组的当前下标

  nextValueIndexInArray=indexInArrays[arrayIdForDeletedData];

  //如果当前下标没有越界,当前数组中还有元素,则查找数组中的下一个元素。

  if (nextValueIndexInArray n) {

  //构造一个新的TrackableData,插入到最小堆中。

  currentInsertedData=新的可跟踪数据(

  arrays . get(arrayIdForDeletedData)[nextValueIndexInArray],

  arrayIdForDeletedData);

  Minh EAP。insert(currentinsertddata);

  //同时更新维护数组当前下标的数组,让对应数组的当前下标一

  indexInArrays[arrayIdForDeletedData];

  }

  //如果当前下标已经越界,说明这个数组已经没有任何元素了,则找下一个有值的数组的最小元素

  否则{

  while (true) {

  arrayIdForDeletedData=(arrayIdForDeletedData 1)% k;

  //获取那个数组的当前下标

  nextValueIndexInArray=indexInArrays[arrayIdForDeletedData];

  if (nextValueIndexInArray==n)

  继续;

  否则{

  //构造新的TrackableData,并且插入到最小堆

  currentInsertedData=新的可跟踪数据(

  数组。get(arrayIdForDeletedData)[nextValueIndexInArray],

  arrayIdForDeletedData);

  Minh EAP。insert(currentinsertddata);

  //同时更新维护数组当前下标的数组,让对应数组的当前下标一

  indexInArrays[arrayIdForDeletedData];

  打破;

  }

  }

  }

  }

  }

  }实验:

  最后我们来演示下,假设我们有32个数,我们分为四路合并,每路8个数,并且这8个数是已经排序的。

  然后我们用K路合并算法来对所有的32个数进行排序:

  公共静态void main(String[] args) {

  //我们来演示K路合并,假设我们有四组已经排序了的数组,每组有8个数,则n=8,k=4

  int[] array1={ 4,5,7,8,66,69,72,79 };

  int[] array2={ 3,9,42,52,53,79,82,87 };

  int[] array3={ 1,17,21,31,47,55,67,95 };

  int[] array4={ 6,28,49,55,68,75,83,94 };

  System.out.println(这里演示K路合并,其中每个数组都事先被排序了,并且长度为8,我们分四路合并);

  System.out.println(数组一为:);

  for(int I=0;IAR射线1。长度;我)

  系统。出去。print(array 1[I] );

  系统。出去。println();

  System.out.println(数组2为:);

  for(int I=0;IAR射线2。长度;我)

  系统。出去。print(array 2[I] );

  系统。出去。println();

  System.out.println(数组3为:);

  for(int I=0;IAR射线3。长度;我)

  系统。出去。print(array 3[I] );

  系统。出去。println();

  System.out.println(数组四为:);

  for(int I=0;IAR射线4。长度;我)

  系统。出去。print(array 4[I] );

  系统。出去。println();

  listint[]arrayLists=new arraylistin[](4);

  arrayLists.add(0,数组1);

  arrayLists.add(1,array 2);

  arrayLists.add(2,array 3);

  arrayLists.add(3,array 4);

  KWayMerger KWayMerger=new KWayMerger(4,8,arrayLists);

  System.out.println(排序后,结果为:);

  kwaymer ger。merge kway();

  系统。出去。println();

  }最后运行结果为:

  写爬虫互联网协议(互联网协议)被封了怎么解决?立即使用

  显然结果是正确的,而且我们的方法是支持重复的值的。以上就是详解K路归并排序(实战)的详细内容,更多请关注我们其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: