简单的分治——归并排序

  分治是一种思想,一种解决问题的方法、手段。从理论上理解起来并不困难:就是把一个大问题分解为若干小问题,逐一解决这些小问题,而后汇总结果。分治思想来指导的算法,都会满足这样的要求:

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;
    }
}

 

posted @ 2016-12-27 11:16  zcsor~流浪dè风  Views(348)  Comments(0Edit  收藏  举报