【蓝桥杯算法笔记】归并排序

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]构成逆序对

③第一种和第二种可以通过递归拆分成第三种的情况,因为我们是归并思想,最终是要把左边递归到只有一个数的区间,这样左边第一个区间和第二个区间就可以算出逆序对的数量。然后再一层一层回溯回去。即这题的递归界限就是当区间只有一个数的时候,回忆归并排序。

④交换顺序会影响第一第二步的计数,但不会影响第三步,由于交换顺序在第一二步后发生,所以不会影响最终结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值