归并排序思想~代码介绍+部分有关题目

应某位 大佬 要求,写一篇归并排序的教程。本分包括:

1.排序(啊啊啊?)

2.时间复杂度

3.归并排序思想(拓展快排思想)

4.归并排序的实现

5.一些很水的题目

 

一.排序(啊啊啊?)

进入第一部分排序的介绍,排序就是对于一个无序的数列,进行一系列操作将其转换为有序序列(包括升序和降序)。首先看两个基本的算法,冒泡排序和选择排序。冒泡排序是什么呢?为什么要叫冒泡排序呢?

所谓冒泡排序就是对于一个数列从第一个数在开始,每次和后面的一个数字进行比较如果我们要将数列排成升序,那么当下一个数字小于当前数的时候,进行一次交换操作(因为这个数应该在这个数后面),因为对于一个数,我们要与后面的进行比较,所以一轮比较后并不能将全部的数变得有序(特殊情况除开),应该要进行n轮比较,才能使序列有序。代码就不给了,因为这不是今天的主角。因为冒泡排序越大的元素会慢慢浮到最后,所以叫做冒泡排序。那么什么是选择排序呢?

对于冒泡排序是一直进行比较,选择排序就转换了一下思路,每次选择最大的出来放在最后(或者最小的放在最前面)。很明显一次循环只能选出一个最大值,所以可以进行n次循环,将序列变得有序,代码依然不给出(实际上这两个算法很好实现,建议初学者,手打一下的)。

二.时间复杂度

对于第二部分是每个OIer必备的一种知识,什么叫做时间复杂度,时间复杂度也就是在给你数据的情况下,你的程序会进行一个怎么阶级的运算次数(解释的不是很好,我们看个例子),就那上面讲的冒泡排序和选择排序来看,对于冒泡排序,给出n个数字,对于第一次for要从1-n每次对于相邻的数进行比较,前面说要for n次那么时间复杂度就是O(n*n)的,实际上也就是要进行n*n条的语句,实际比赛是应该大致估算,对于比赛时,实现是1s而比赛设置的1s运算次数为1e8也就是1亿。对于选择排序同样很容易知道复杂度为O(n*n)。

三.归并排序思想(拓展快排思想)、

终于讲到重点了,关于归并排序所用到的思想是一个很重要的思想叫做分治,所谓的分治就是分而治之,讲一个大的问题,分成小问题进行解决。那么对于排序我们可以这样想,是不是可以将一个大的序列,分成两个长度尽量相等的序列(如果一开始是偶数,则两边元素相等,否则其中一边多一个就行),然后我们对于每一边进行排序,最后在合并就可以达成一个有序的序列。实际上这就是归并排序的思想:

1.将大的序列,分成小的序列

2.对小的序列进行排序

3.进行合并,使合并后的序列有序

拓展快速排序的思想:

1.将大的序列分成两个小的序列,并且保证左边的每一个数都比右边的数小

2.将左右区间进行排序

3.合并(不需要过多操作因为左边的有序, 右边的有序,左边的又比右边的小,合并后肯定有序)

实现就是找一个基本量从两边开始循环进行很多次交换直到左边的都比右边的小关于快速排序,代码就不给了。另外,快速排序的复杂度不稳定,根标兵的选取有关,但是C++库函数里的sort的复杂度就很NB,据说是怎么快怎么排。

四.归并排序的实现

实际上思想很好懂,但是第2步,第3步并不是很好实现(尤其是第二步),实际上我们可以这样想,我们一直分呀分,分到左右两边只有一个元素时,那么对于单个的元素是有序的,我们考虑合并,只需要将小的放在前面,大的放在后面就行,对于分成两个含有两个元素的序列,因为我们是递归实现所以,我们已经保证了这两个序列有序,那么这个时候我们进行合并,我们需要知道当前数组属于哪个区间我们设为[l,r)左闭右开,那么左边的就为[l,mid)右边就为[mid,r)那么左右两个区间是有序的,我们创建一个数组t,然后从l和mid开始,将位于l的元素与位于mid的元素进行比较,我们选择较为小的数放在t[l]位置然后将选择的数所在的区间向右移,如果其中一个区间没有数字了,就直接放另一个区间的数字到t中这样下来,t中的元素就是从小到大的了,我们再将t复制给a,这样就实现了合并操作。

/*
以下为合并操作
*/
int x = l, y = mid, z = l;//为了不影响下一次合并,应将l,r,mid赋值给另外的变量
    while(x < mid || y < r){ //左区间或者右区间不为空,就继续
        if (y >= r || (x < mid && a[x] <= a[y])) t[z ++] = a[x ++];//表示左边的数较小,或者右区间为空
        else t[z ++] = a[y ++];//与上边不符合就代表右边的数较小,或者左区间为空
    }
    for (int i = l;i < r;i ++) a[i] = t[i];//将t复制给a

那么最难的合并操作都完成了完整代码还会难吗?

/*
以下是归并排序左闭右开
*/
void merge_sort(int *a,int l,int r,int *t){
    if (r - l <= 1) return ;//只有一个元素时直接返回,因为本身有序
    int mid = (l + r) >> 1;
    merge_sort(a, l, mid, t);//划分左区间
    merge_sort(a, mid, r, t);//划分右区间
    int x = l, y = mid, z = l;
    while(x < mid || y < r){
        if (y >= r || (x < mid && a[x] <= a[y])) t[z ++] = a[x ++];
        else t[z ++] = a[y ++];
    }
    for (int i = l;i < r;i ++) a[i] = t[i];
}

 五.一些很水的题目


1.LUOGU 1177

题意:给你n个数,对其进行排序。

分析:本来是是一道快排的模板题,但是我用快排怎么写都是T了三个点(一共五个点),可能是我快排写的有问题,用STL的sort就可以过,但是,竟然会了归并排序,就用归并排序水一水吧。

 

2.LUOGU 1908

题意:给你n个数,求出其逆序对(我觉得题目改成求冒泡排序的交换次数要好一点,不至于那么直接)

分析:对于冒泡排序的交换次数,我们如果模拟是O(n*n)的复杂度,很明显TLE,我们想一想,我们冒泡时怎么才会交换,只有i<j,a[i]>a[j]时才回交换,这也就是这道题所问的逆序对:那么对于求逆序对,我们可以借助归并排序,将序列分开求两边的逆序对,在进行合并。当然这道题,可以用树状数组离散数据了处理复杂度是一样的但是归并要好写的多。

代码:

#include <cstdio>
#include <iostream>

const int maxn = 100010;

using namespace std;

int n, cnt;
int a[maxn], t[maxn];

void mergesort(int *a,int l,int r,int *t){
    if(r - l <= 1) return ;
    int m = (l + r) >> 1;
    mergesort(a, l, m, t);
    mergesort(a, m, r, t);
    int x = l, y = m, z = l;
    while(x < m || y < r){
        if (y >= r || (a[x] <= a[y] && x < m)) t[z++] = a[x++];
        else t[z++] = a[y++], cnt += m - x; //存在逆序对,这里不能写成cnt++因为左边序列是有序的,
//右边序列也是有序的,左边有一位比右边的这一位大了,那么左边后边(一直到mid)也一定比这一位大,所以要加mid-x,注意这里写的归并依然是左闭右开
} for (int i = l;i < y;i ++) a[i] = t[i]; } int main(){ scanf("%d", &n); for (int i = 1;i <= n;i ++) scanf("%d", &a[i]); mergesort(a, 1, n+1, t); printf("%d ", cnt); return 0; }

 

3.LUOGU 1966

题意:不好描述,大家看题面吧。

分析:首先这道题要用到贪心的思想:那就是,对于距离最小必须保证小的和小的在一起,大的和大的在一起,证明这里不给出。知道了这样一个重要的结论似乎还是不好做,我们考虑将a中最小的与b中最小的对在一起,第二小的和第二小的对在一起,我们让b不动,让a来移动模拟一遍,很明显超时。现在想想怎么优化,我们记录a的数据和本身的位置,记录b的数据和本身的位置,然后对他们分别按照高度排序。这样小的就对小的了,大的就对大的了,并且我们还有了他们之前的位置。然后我们用一个数组loc记loc[i] = j表示a中第i小的数应该在b中第i小的数的位置,这实际上是一个映射。映射之后对应的就是位置我们的目的就变成了将每一loc[i]进行交换将其变成loc[i] = i的形式,这也就是冒泡排序要交换多少次,也就是求解逆序对。

代码:

#include <cstdio>
#include <iostream>
#include <algorithm>

const int maxn = 100010;
const int mod  = 99999997;

using namespace std;

typedef long long ll;

int n, ans;
int c[maxn], t[maxn];

struct node{
    ll val;
    int pos;
}a[maxn], b[maxn];
bool cmp(const node a,const node b){return a.val < b.val;}
void merge_sort(int *a,int l,int r,int *t){
    if (r - l <= 1) return ;
    int mid = (l + r) >> 1;
    merge_sort(a, l, mid, t);
    merge_sort(a, mid, r, t);
    int x = l, y = mid, z = l;
    while(x < mid || y < r){
        if (y >= r || (x < mid && a[x] <= a[y])) t[z ++] = a[x ++];
        else{
            t[z ++] = a[y ++];
            ans = (ans + mid - x) % mod;
        }
    }
    for (int i = l;i < r;i ++) a[i] = t[i];
}
int main(){
    scanf("%d", &n);
    for (int i = 1;i <= n;i ++){
        scanf("%lld", &a[i].val);
        a[i].pos = i;
    }
    for (int i = 1;i <= n;i ++){
        scanf("%lld", &b[i].val);
        b[i].pos = i;
    }
    sort(a+1, a+1+n, cmp);
    sort(b+1, b+1+n, cmp);
    for (int i = 1;i <= n;i ++) c[a[i].pos] = b[i].pos;//进行映射
    merge_sort(c, 1, n+1, t);
    printf("%d", ans);
    return 0;
}

总结:归并排序的思想很重要,对于一些关于序列的问题,要用到逆序对和归并排序的思想。

posted @ 2017-10-17 21:58  Kaiser(蛰伏中)  阅读(890)  评论(0编辑  收藏  举报