简单的分治——归并排序
分治是一种思想,一种解决问题的方法、手段。从理论上理解起来并不困难:就是把一个大问题分解为若干小问题,逐一解决这些小问题,而后汇总结果。分治思想来指导的算法,都会满足这样的要求:
1、如果问题能分解为子问题,并且子问题和原问题可以用同一种方法解决。当然,如果子问题还比较复杂,那么可以继续分解。
2、子问题的解可以合并为原问题的解。
如果不满足这些要求,那么要么是子问题划分不合理,要么只能用分支结构来解决。分支结构何尝不是一种分治思想的体现呢。所以,分治算法适合于解决那些子问题有共同解法的,解可以合并的问题,当问题规模较大时——分解为若干层次子问题时,分治是一种解决问题的重要思路。归并排序就是一个利用分治来解决问题的很好例子,对于一个数组,我们首先把它进行分解为若干段,然后对这些段进行排序,最后合并结果即可。这是基本的分治思路,那么现在考虑一下具体问题:
1、子问题的最小规模是多大?即分解到一段多少数据:最小规模就是1段1个数据,解决方法简单到无需比较就已经排序好了。那么,划分为2个可不可以?答案是不可以,因为那样做引入了(或者说没有消除)段内数据比较的问题,编码将更加复杂。当然,如果非要那样做也不是不能实现。
2、如何合并结果?第一次合并时每段只有一个元素,只需要按次序存放即可;下次合并时因为每段已经有次序,所以只需要比较两段中第一个元素,把较小的先拿出来,这样就不需要再次访问段中其他元素了。
3、如何设计程序结构?因为我们要分解和合并,所以需要若干个循环过程,从之前的分析来看,整个算法不断的重复执行分解、而后不断的进行合并,可以用循环或递归来实现。因为递归在程序编码上较简单,所以这里采取递归的结构:
void mmsort(int arr[],int leftid,int rightid){ int mid; //当leftid=rightid时,划分为一段只有一个元素。否则继续划分为左右两部分。 if(leftid<rightid){ mid=leftid+(rightid-leftid)/2; mmsort(arr,leftid,mid); mmsort(arr,mid+1,rightid); //划分完毕之后进行合并。 mmerge(arr,leftid,mid,rightid); } }
程序结构非常清晰,当leftid=rightid时,此段只有一个元素达到终点——函数返回(代码中没有写入else部分,因为它没有必要写出来);当leftid<rightid时,用二分的方法将数据继续分段。当左侧分段结束之后,对右侧进行分段,两侧分段结束之后,进行合并。接下来,需要对合并函数进行编码,这时应该考虑一些细节:
1、关于临时存储合并结果段:若我们按升序排序,现在有两段:4,5和2,7我们取出最小的2,它的位置是最左面,如果直接写回数组,则4被覆盖——消失了。这可真糟,所以归并过程中,我们需要一个临时数组保存2,4,5,7这两段合并(并排序)的结果,当这两段合一(并排序)之后,把临时数组写回。
2、关于被合并段不等长:这是一种经常出现的情况,按照我们的做法:先取两段数据中最小的(左段或右段的第一个数据,因为我们之前合并时已经对两段数据排序。),然后第二小的……总有一段取完而另一段还有数据。易知,没有取完的部分一定大于已经取完的部分,所以只需要把这些没有取到的数据直接连接到临时数组后面。
剩下的就是编码,并不难:
void mmerge(int arr[],int leftid,int mid,int rightid){ int i; //将左右两个段进行合并时,直接使用原数组可能产生覆盖,所以先排序到临时数组。 int *tmparr=new int[rightid-leftid+1]; int tmparrcurid=0; int leftarrcurid=leftid; int rightarrcurid=mid+1; //进行比较,在临时数组中保存排序结果。此时左右段长度不一定相同。 //所以需要分别处理:左右段都有元素时进行比较,将较小的存入临时数组; //当一段没有元素了,说明另一段剩下的元素都比现在临时数组中的大,直接连接在临时数组后面。 while(leftarrcurid<=mid && rightarrcurid<=rightid){ //两个段都有元素时 if(arr[leftarrcurid]>arr[rightarrcurid]){ //把最大的存入临时数组 tmparr[tmparrcurid]=arr[leftarrcurid]; leftarrcurid++; }else{ tmparr[tmparrcurid]=arr[rightarrcurid]; rightarrcurid++; } tmparrcurid++; } //此时有一个段没有元素了,把另一个剩下的元素复制到tmparr中。 for(i=leftarrcurid;i<=mid;i++){ tmparr[tmparrcurid]=arr[i]; tmparrcurid++; } for(i=rightarrcurid;i<=rightid;i++){ tmparr[tmparrcurid]=arr[i]; tmparrcurid++; } //最后,把排序好的leftid到rightid段从tmparr复制回来。 tmparrcurid=0; for(i=leftid;i<=rightid;i++){ arr[i]=tmparr[tmparrcurid]; tmparrcurid++; } delete []tmparr; }
虽然命名时使用了arr字样,但这个arr是虚指,并没有真的去声明一个数组实例来存储数据,根本上来讲,所指的就是把原始数组中leftid和rightid之间的部分看作一个数组。出于这个原因,在文章中我一直使用“段”这个字,而不是“数组”。另:上述代码是减序排列。
这次解决的NOI题目是:
7617:输出前k大的数 查看 提交 统计 提问 总时间限制: 10000ms 单个测试点时间限制: 1000ms 内存限制: 65536kB 描述 给定一个数组,统计前k大的数并且把这k个数从大到小输出。 输入 第一行包含一个整数n,表示数组的大小。n < 100000。 第二行包含n个整数,表示数组的元素,整数之间以一个空格分开。每个整数的绝对值不超过100000000。 第三行包含一个整数k。k < n。 输出 从大到小输出前k大的数,每个数一行。 样例输入 10 4 5 6 9 8 7 1 2 3 0 5 样例输出 9 8 7 6 5
再加上main函数就是完整的解题代码了(头文件略):
int main(){ int arrsize,cnt,i; cin>>arrsize; int arr[arrsize]; for(i=0;i<arrsize;i++){ cin>>arr[i]; } cin>>cnt; mmsort(arr,0,arrsize-1); for(i=0;i<cnt;i++){ cout<<arr[i]<<endl; } }