分治法--归并排序,快速排序,求排列的逆序数

 分治法:

分治法(divide-and-conquer):将原问题划分成n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。

   分治模式在每一层递归上都有三个步骤:

  • 分解(Divide):将原问题分解成一系列子问题;
  • 解决(conquer):递归地解各个子问题。若子问题足够小,则直接求解;
  • 合并(Combine):将子问题的结果合并成原问题的解。

分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

分治法的精髓:

分--将问题分解为规模更小的子问题;

治--将这些规模更小的子问题逐个击破;

合--将已解决的子问题合并,最终得出“母”问题的解;

分治法所能解决的问题一般具有以下几个特征:

1) 该问题的规模缩小到一定的程度就可以容易地解决

2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

3) 利用该问题分解出的子问题的解可以合并为该问题的解;

4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

上述的第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。

1.分治的典型应用之一:归并排序 

数组排序任务可以如下完成:

(1)前一半排序

(2)后一半排序

(3)把两半归并到一个新的有序数组,再拷贝回原数组。

对每一半都递归采用上述方法。

即:

  • 分解:将n个元素分成各含n/2个元素的子序列;
  • 解决:用合并排序法对两个子序列递归地排序;
  • 合并:合并两个已排序的子序列以得到排序结果。
#include<iostream>
using namespace std;
void merge(int a[],int s,int m,int e,int tmp[])//将数组a的局部a[s,m]和a[m+1,e]合并到tmp[],并保证temp有序,然后再拷贝回a[s,e]。注意a[s,m]和a[m+1,e]都各自已经是有序了的
{
	int p=0;//p在tmp数组上移动 
	int p1=s,p2=m+1;//p1在排好序了的前半段数组上移动,p2在排好序了的后半段数组上移动, 
	while(p1<=m&&p2<=e)
	{
		if(a[p1]<a[p2])
		{
			tmp[p]=a[p1];
			p++;
			p1++;//上面三行可以简写为tmp[p++]=a[p1++]; 
		} 
		else
		{
			tmp[p]=a[p2];
			p++;
			p2++;
		}
	}
	while(p1<=m)//若p2走到头了,p1还没有走到头 
	{
		tmp[p]=a[p1];
		p++;
		p1++;
	}
	while(p2<=e)//若p1走到头了,p2还没有走到头 
	{
		tmp[p]=a[p2];
		p++;
		p2++;
	}
	for(int i=0;i<e-s+1;i++)//将这两部分排好序放在tmp里了后,再拷贝回a[s,e]注意是a[s,e](一部分),而不是全部a[0,size-1] 
	{
		a[s+i]=tmp[i];
	}
}
void mergesort(int a[],int s,int e,int tmp[]) 
{
	if(s==e)//边界条件,s=e, 也就是头和尾相等,指向的元素是同一个,即数组只有一个元素,此时直接return 
		return;
	if(s<e)
	{
		int m=s+(e-s)/2;//数组中间元素 
		mergesort(a,s,m,tmp);
		mergesort(a,m+1,e,tmp);
		merge(a,s,m,e,tmp);
	}

}
int main()
{
	int a[10],b[10];
	for(int i=0;i<10;i++)
	{
		cin>>a[i];
	}
	int size=sizeof(a)/sizeof(int);//待排序的a数组的元素个数 
	mergesort(a,0,size-1,b);
	for(int i=0;i<size;i++)
	{
		cout<<a[i]<<" ";
	}
	cout<<endl;	
	return 0;
} 

 

2.分治的典型应用之二:快速排序 

数组排序任务可以如下完成:

  (1)设k=a[0],将k挪到适当位置,使得比k小的元素都在k左边,比k大的元素都在k右边,和k相等的,不关心在k左右出现均可(0 (n) 时间完成)。

(2)把k左边的部分快速排序。

(3)把k右边的部分快速排序。

对左右两边的都递归采用上述方法。

#include<iostream>
using namespace std;
void swap(int & a,int & b)//交换变量a,b的值,传址方式,参数是引用(注意要加&,这样形参的值改变的话实参的值才会跟着改变) 
{
	int tmp=a;
	a=b;
	b=tmp;
}
void Quicksort(int a[],int s,int e) 
{
	if(s==e)//边界条件,s=e, 也就是头和尾相等,指向的元素是同一个,即数组只有一个元素,此时直接return 
		return;
	if(s<e)
	{
		int k=a[s];//a数组一部分的第一个元素 
		int i=s,j=e;//i从头开始,负责前一半;j从尾开始,负责后一半 
		while(i!=j)
		{
			while(i<j&&a[j]>=k)
			{
				j--;
			}
			swap(a[i],a[j]);//从while循环中出来,可能因为a[j]<k,这时要交换一下;
							//还可能因为i=j,i和j指向同一个元素,交换一下相当于没交换,无影响 
			while(i<j&&a[i]<=k)
			{
				i++;
			}
			swap(a[i],a[j]);//从while循环中出来,可能因为a[i]>k,这时要交换一下;
							//还可能因为i=j,i和j指向同一个元素,交换一下相当于没交换,无影响 
		}//做完后a[i]=a[j]=k; 
		Quicksort(a,s,i-1);
		Quicksort(a,i+1,e);
	}

}
int main()
{
	int a[10];
	for(int i=0;i<10;i++)
	{
		cin>>a[i];
	}
	int size=sizeof(a)/sizeof(int);//待排序的a数组的元素个数 
	Quicksort(a,0,size-1);
	for(int i=0;i<size;i++)
	{
		cout<<a[i]<<" ";
	}
	cout<<endl;	
	return 0;
} 

3.求排列的逆序数

考虑1,2,..,n (n <=,100000) 的排列R1,R2,... Rn, 如果其中存在j,k,满足j < k  且  Rj> Rk,  那么就称(Rj,Rk)是这个排列的一个逆序。

一个排列含有逆序的个数称为这个排列的逆序数。例如排列263451 含有8个逆序(2,1),(6,3), (6,4), (6,5), (6, 1), (3,1),(4,1), (5,1),因此该排列的逆序数就是8。

现给定1,2,...,n的一个排列,求它的逆序数。

输入:
第一行是一个整数n,表示该排列有n个数(n <= 100000)。 
第二行是n个不同的正整数,之间以空格隔开,表示该排列。 
输出:
输出该排列的逆序数。 
样例输入 

2 6 3 4 5 1 
样例输出 

提示 
1. 利用二分归并排序算法(分治); 
2. 注意结果可能超过int的范围,需要用long long存储。 
解题思路: 

1.寻找逆序对的数量,最容易想到的便是双for循环寻找,为O(n^2),效率太低,容易超时,如何将线性查找简化,就是分治,如何利用分治进行查找才是关键。 

2.分治O(nlogn)

1.将数组分成两半,分别对左半端和右半段进行求逆序数。

2.再计算有多少个逆序数是由左半段取一个数和右半段取一个数构成(要求O(n)实现)。

步骤2的关键:左半边和右半边都是排好序的。比如,都是从大到小排序的。这样,左右半边只需要从头到尾各扫一遍,就可以找出由两边各取一个数构成的逆序个数。

/*样例输入
6
2 6 3 4 5 1
样例输出
8
样例输入
8
10 3 7 8 12 11 5 2 
样例输出
16
*/
#include<iostream>
using namespace std;
const int maxn=100005;
int a[maxn];
int b[maxn];
long long sum=0;//注意结果可能超过int的范围,需要用long long存储。
void MergeAndCount(int a[],int s,int m,int e,int tmp[])//计算前半段的一个数和后半段的一个数能构成多少个逆序数,并且将数组a的局部a[s,m]和a[m+1,e]合并到tmp[],并保证temp从大到小有序,然后再拷贝回a[s,e]。注意a[s,m]和a[m+1,e]都各自已经是有序了的 
{
	int p=0;//p在tmp数组上移动 
	int p1=s,p2=m+1;//p1在排好序了的前半段数组上移动,p2在排好序了的后半段数组上移动
	while(p1<=m&&p2<=e)
	{
		if(a[p1]<=a[p2])//如果左半边的当前这个小于右半边的当前这个(即不能构成逆序数),那么左半边后边的肯定更不能构成(更小) 
		{
			tmp[p]=a[p2];//因为是从大到小排列,故tmp赋值为大的那个
			p++; 
			p2++;
		}
		else//如果大于 
		{
			tmp[p]=a[p1];
			sum+=(e-p2+1);
			p++;
			p1++;
		}
	}
	while(p1<=m)//若p2走到头了,p1还没有走到头 
	{
		tmp[p]=a[p1];
		p++;
		p1++;
	}
	while(p2<=e)//若p1走到头了,p2还没有走到头 
	{
		tmp[p]=a[p2];
		p++;
		p2++;
	}
	for(int i=0;i<e-s+1;i++)
	{
		a[s+i]=tmp[i];
	}
}
void MergeSort(int a[],int s,int e,int tmp[])
{
	if(s==e)
	{
		return;
	}
	if(s<e)
	{
		int m=s+(e-s)/2;
		MergeSort(a,s,m,tmp);
		MergeSort(a,m+1,e,tmp);
		MergeAndCount(a,s,m,e,tmp);
	}
}
int main()
{
	int n;
	cin>>n;
	for(int i=0;i<n;i++)
	{
		cin>>a[i];
	}
	MergeSort(a,0,n-1,b);
	/*以下是为了看看是否从大到小排列了*/
	for(int i=0;i<n;i++)
	{
		cout<<a[i];
	}
	cout<<endl;
	cout<<sum<<endl;
	return 0;
}

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值