I.归并排序
基础知识
1.算法思路:
核心思想:分治
利用划分子序列的方法递归实现。首先把整个待排序序列划分为两个长度大致相等的子序列,对这两个子序列分别递归地进行排序,然后再把他们归并
重点操作:归并
将待排序的序列中前后相邻的两个有序序列归并为一个有序序列
过程:
(1)确定分界点:mid=(l+r)/2
将当前序列一分为二,求出分裂点mid=(l+r)/2
(2)递归归并排序左区间、右区间
通过一层一层地递归,可以将一开始地左右两段区间序列再次左右拆分,知道递归搜索拆分成前后相邻的几个有序序列为止,然后再回溯一步步执行下面的比较合并
(3)归并——合二为一(难点),将两个有序序列合二为一
实现:双指针算法
对于已知的两个排好序的有序序列,并且他们是相邻的。设有两个指针(表示下标)分别指向各自序列中的最小值。每次都是从两个序列中找指针所指的最小值进行比较(两个指针一直都指向所在序列中的最小值),将较小者放到开好的结果数组里,然后该指针向后移动一次,重复此过程,知道其中一个表序列为空,最后将另一非空表中余下的部分直接复制到结果数组中。
2.代码模板:
int q[N];
int t[N];
void mergesort(int q[],int l,int r) //如果q[]定义为全局数组,可以在传递参数时省略void mergesort(int l,int r)
{
if(l>=r) return; //递归边界:说明当前区间中元素个数为一个或者一个都没有了,return掉
//确定分界点
int mid=(l+r)>>1;
//递归排序左区间、右区间
mergesort(q,l,mid);
mergesort(q,mid+1,r);
//归并
int k=0,i=l,j=mid+1; //k表示当前t结果数组中已经存多少数了,为下标遍历;i和j分别代表两个指针下标,i指向左半边起点,j指向右半边起点
while(i<=mid&&j<=r)//判断两个序列中当前值谁更小,把较小的放到t[]数组中
{
if(q[i]<q[j])
t[k++]=q[i++];
else
t[k++]=q[j++];
}
while(i<=mid) t[k++]=q[i++]; //如果剩余左半边序列非空,则将其余下的部分直接复制到结果数组中。
while(j<=r) t[k++]=q[j++]; //如果剩余右半边序列非空,则将其余下的部分直接复制到结果数组中。
//因为结果存到t[]中去了,再将其复制到q[]数组中
for(int i=l,j=0;i<=r;i++,j++) q[i]=t[j];
//这一步也可以省略,在main()函数中输出时直接将t[]结果数组输出也可
}
3.时间复杂度:
O(nlogn)
4.稳定性
归并排序是稳定的
如何看一个排序稳定不稳定?
一个排序算法是稳定的并不是说它的时间效率是稳定的。稳定是指如果原序列中的两个值相同的,经过排序之后,它们的位置没有发生变化,那么这个排序就是稳定的。它们的位置如果可能会发生变化,那么这个排序就是不稳定的。
5.与快速排序的区别与联系:
①快排的主要思想也是分治,但与归并排序的分治方法不同。
快排的分界点是用一个数x(数组里随机一个数值)来分,分完之后让左边都是小于等于x,右边都是大于等于x;而归并排序的分界点是以整个数组的中心位置(下标的中间值),分为左区间和右区间。
②快排的最复杂的核心操作是第二步划分:把当前区间划分为两段;而归并排序最复杂的核心操作是第三步归并:把各个相邻的有序序列合并成一个序列。
③快排是先划分两边,然后递归排序;而归并排序是先递归排序左右区间序列,然后合并
④快排和归并排序的时间复杂度都是O(nlogn)。
归并排序的时间复杂度是妥妥的O(nlogn),而快速排序的平均时间复杂度是O(nlogn),最坏时间复杂度为O(n*n)
⑤快排是不稳定的,而归并排序是是稳定的。
例题
一、归并排序
1.基本思路
如上
2.代码
#include <iostream>
using namespace std;
const int N=100010;
int q[N];
int t[N];
void mergesort(int q[],int l,int r) //如果q[]定义为全局数组,可以在传递参数时省略void mergesort(int l,int r)
{
if(l>=r) return; //递归边界:说明当前区间中元素个数为一个或者一个都没有了,return掉
//确定分界点
int mid=(l+r)>>1;
//递归排序左区间、右区间
mergesort(q,l,mid);
mergesort(q,mid+1,r);
//归并
int k=0,i=l,j=mid+1; //k表示当前t结果数组中已经存多少数了,为下标遍历;i和j分别代表两个指针下标,i指向左半边起点,j指向右半边起点
while(i<=mid&&j<=r)//判断两个序列中当前值谁更小,把较小的放到t[]数组中
{
if(q[i]<q[j])
t[k++]=q[i++];
else
t[k++]=q[j++];
}
while(i<=mid) t[k++]=q[i++]; //如果剩余左半边序列非空,则将其余下的部分直接复制到结果数组中。
while(j<=r) t[k++]=q[j++]; //如果剩余右半边序列非空,则将其余下的部分直接复制到结果数组中。
//因为结果存到t[]中去了,再将其复制到q[]数组中
for(int i=l,j=0;i<=r;i++,j++) q[i]=t[j];
//这一步也可以省略,在main()函数中输出时直接将t[]结果数组输出也可
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>q[i];
mergesort(q,0,n-1);
for(int i=0;i<n;i++) cout<<q[i]<<" ";
//for(int i=0;i<n;i++) cout<<t[i]<<" ";
return 0;
}
二、逆序对的数量
1.基本思路
逆序对是在排序的过程中求中的。
一共分成三类:
①两个元素都在左边;
②两个元素都在右边;
③两个元素一个在左一个在右;
其中前两类可以递归,另一类在归并的时候可以求,所以只需在排序计算第三种情况时加一个统计即可。
为什么可以排序?
这个时候我们注意到一个很重要的性质,左右半边的元素在各自任意调换顺序,是不影响第三步计数的,因此我们可以数完就给它排序。这么做的好处在于,如果序列是有序的,会让第三步计数很容易。
交换顺序会影响第一第二步的计数,但不会影响第三步,由于交换顺序在第一二步后发生,所以不会影响最终结果。
为什么第一种和第二种情况就可以自动算出来呢?
第三步理解了就好了,因为我们是归并思想,最终是要把左边递归到只有一个数的区间,这样左边第一个区间和第二个区间就可以算出逆序对的数量。然后再一层一层回溯回去。即这题的递归界限就是当区间只有一个数的时候,回忆归并排序。
2.代码
//暴力做法,会超时
#include <iostream>
using namespace std;
const int N=100010;
int q[N];
int n;
long long int res;
int main()
{
cin >> n;
for(int i=0;i<n;i ++) cin>>q[i];
for(int i=0;i<n-1;i ++)
{
for(int j=i+1;j<n;j++)
{
if(q[i]>q[j])
{
res++;
}
}
}
cout<<res;
return 0;
}
//归并排序解法
#include <iostream>
using namespace std;
const int N=100010;
int q[N];
int t[N];
long long int res;//注意范围
void mergesort(int q[],int l,int r) //如果q[]定义为全局数组,可以在传递参数时省略void mergesort(int l,int r)
{
if(l>=r) return; //递归边界:说明当前区间中元素个数为一个或者一个都没有了,return掉
//确定分界点
int mid=(l+r)>>1;
//递归排序左区间、右区间
mergesort(q,l,mid);
mergesort(q,mid+1,r);
//归并
int k=0,i=l,j=mid+1; //k表示当前t结果数组中已经存多少数了,为下标遍历;i和j分别代表两个指针下标,i指向左半边起点,j指向右半边起点
while(i<=mid&&j<=r)//判断两个序列中当前值谁更小,把较小的放到t[]数组中
{
if(q[i]<=q[j]) //注意这里一定是小于等于,少了等号会导致相同的数也被计算了数对
{
t[k++]=q[i++];
}
else //在归并的过程中 i < j,此时q[i]>q[j],这样在[i,mid]中的数都与q[j]构成逆序对
{
t[k++]=q[j++];
res+=mid-i+1; //统计
}
}
while(i<=mid) t[k++]=q[i++]; //如果剩余左半边序列非空,则将其余下的部分直接复制到结果数组中。
while(j<=r) t[k++]=q[j++]; //如果剩余右半边序列非空,则将其余下的部分直接复制到结果数组中。
//因为结果存到t[]中去了,再将其复制到q[]数组中
for(int i=l,j=0;i<=r;i++,j++) q[i]=t[j];
//这一步也可以省略,在main()函数中输出时直接将t[]结果数组输出也可
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>q[i];
mergesort(q,0,n-1);
cout<<res;
return 0;
}
3.注意:
①if(q[i]<=q[j]) //注意这里一定是小于等于,少了等号会导致相同的数也被计算了数对
②在归并的过程中 i < j,此时q[i]>q[j],这样在[i,mid]中的数都与q[j]构成逆序对
③第一种和第二种可以通过递归拆分成第三种的情况,因为我们是归并思想,最终是要把左边递归到只有一个数的区间,这样左边第一个区间和第二个区间就可以算出逆序对的数量。然后再一层一层回溯回去。即这题的递归界限就是当区间只有一个数的时候,回忆归并排序。
④交换顺序会影响第一第二步的计数,但不会影响第三步,由于交换顺序在第一二步后发生,所以不会影响最终结果。