ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【蓝桥杯专题】I.归并排序

2022-03-20 20:32:10  阅读:136  来源: 互联网

标签:归并 递归 int mid 蓝桥 序列 排序


文章目录

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

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

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

标签:归并,递归,int,mid,蓝桥,序列,排序
来源: https://blog.csdn.net/qq_46009744/article/details/123620534

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有