ps:无语无语无语,递归不要再搞我了。。。
分治基本概念
把一个任务,分成形式与原任务相同,但规模更小的几个部分任务(通常使两个部分),分别完成,或只需要选一步完成。然后在处理完成后的这一个或几个部分的结果,实现整个任务的完成。
分治的典型应用:归并排序
- 数组排序任务可以如下完成:
1) 把前一半排序
2)把后一半排序
3)把两半归并到一个新的有序数组,然后再拷贝回原数组,排序完成
归并排序的时间复杂度
对n个元素进行排序的时间:
T(n) = 2*T(n/2) + a*n //两个分别排序+归并(a是常数,具体多少不重要)
= 2*(2*T(n/4)+a*n/2)+a*n
= 4*T(n/4)+2a*n
= 4*(2*T(n/8)+a*n/4)+2*a*n
= 8*T(n/8)+3*a*n
...
= 2^k*T(n/2^k)+k*a*n
一直做到n/2^k = 1(此时k = log2n),T(n) = 2^k+k*a*n = 2^k*T(1)+k*a*n = 2^k+k*a*n = n+a*(log2n)*n
复杂度O(nlogn)
代码举例:归并排序
#include<iostream>
using namespace std;
int a[10] = { 13,27,19,2,8,12,2,8,30,89};
int b[10];
void Merge(int a[],int s, int m, int e, int tmp[])
{
// 将数组a的局部a[s,m]和a[m+1,e]合并到tmp,并保证tmp有序,然后再拷贝回a[s,m]
//归并操作时间复杂度:0(e-m+1),即O(n)
int pb = 0;
int p1 = s, p2 = m+1;
while(p1 <= m && p2 <= e) { //两个指针都没有到头,比较其中元素,插入小值
if(a[p1] < a[p2])
tmp[pb++] = a[p1++];
else
tmp[pb++] = a[p2++];
}// 比较完后把剩余内容全部拷贝
while(p1 <= m)
tmp[pb++] = a[p1++];
while(p2 <= e)
tmp[pb++] = a[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[])
{ //a[s]到a[e]连续排序,tmp[]做中转
if(s < e) {// s = e直接 return
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 size = sizeof(a)/sizeof(int);
MergeSort(a,0,size-1,b); // 排好序的两个数组合并到一起(可以只排中间一段)
for(int i = 0; i < size; ++i)
cout<<a[i]<<",";
cout << endl;
return 0;
}
总结:
- 语法问题
1)sizeof(a)/sizeof(int)
sizeof(int)计算int型变量占内存单元
sizeof(a) 计算 整型数组里元素 占用内存多少单元
所以sizeof(a)/sizeof(int)计算元素个数 - 细节问题
1)merge()函数中,注意排序后要复制回原数组 - 逻辑问题
1)merge()函数负责数组比较归并,mergesort()函数负责整体排序
分治的典型应用:快速排序
- 数组排序任务可以如下完成:
1)设k = a[0],将k挪到适当位置,使得比k小的元素都在k左边,比k大的元素都在k右边,和k相等的,不关心在k左右出现均可(O(n)时间完成)
2)把k左边的部分快速排序
3)把k右边的部分快速排序
代码举例:快速排序
#include<iostream>
using namespace std;
int a[] = {93,27,30,2,8,30,89};
void swap(int & a,int & b) //交换变量a,b值
{// 增加引用符,改a,b, c,d也会变化
int tmp = a;
a = b;
b = tmp;
}
void QuickSort(int a[],int s, int e)
{// 数组a[],起点s,终点e
if(s >= e) //待排序只有一个元素
return;
int k = a[s];
int i = s, j = e;
while(i != j) { //比较a[i]和a[j]
while(j > i && a[j] >= k)
--j;
swap(a[i],a[j]);
while(j < i && a[i] <= k)
++i;
swap(a[i],a[j]);
}//处理完以后,a[i] = k
QuickSort(a,s,i-1); //k左边进行快速排序
QuickSort(a,i+1,e); //k右边进行快速排序
}
int main()
{
int size = sizeof(a)/sizeof(int);
QuickSort(a,0,size-1);
for(int i = 0;i < size; ++i)
cout << a[i] << ",";
cout << endl;
return 0;
}
// 运气不坏:两边数目差不多
// 运气坏:左边0个或右边0个
总结
- 语法问题
1)注意swap函数内参数
2)考虑s>=e的情况
课后练习1:输出前k大的数
描述:
给定一个数组,统计前k大的数并且把这k个数从大到小输出。
输入:
第一行包含一个整数n,表示数组的大小。n < 100000。
第二行包含n个整数,表示数组的元素,整数之间以一个空格分开。每个整数的绝对值不超过100000000。
第三行包含一个整数k。k < n。
输出:
从大到小输出前k大的数,每个数一行。
解题思路:
从大到小输出前k大的数,每个数一行
排序后再输出,复杂度O(nlogn)
用分治处理:复杂度O(n+klogk) //确保m << n
思路:把前k大的都弄到数组最右边,然后对这最右边k个元素排序,再输出
关键:O(n)时间内实现把前k大的都弄到数组最右边
引入操作arrangeRight(k):把数组(或数组的一部分)前k大的都弄到最右边
1)设key = a[0],将key挪到适当位置,使得比key小的元素都在k的左边,比key大的元素都在key右边(线性时间完成)
2)选择数组的前部或后部再进行arrangeRight操作
a = k done
a > k 对此a个元素再进行arrangeRigth(k)
a < k 对左边b个元素再进行arrangeRight(k-a)
将前k大的都弄到数组最右边的时间:
T(n) = T(n/2) + a*n
= T(n/4) + a*n/2 + a*n
= T(n/8) + a*n/4 + a*n/2 + a*n
= ...
= T(1) + ... + a*n/8 + a*n/4 + a*n/2 + a*n
< 2*a*n
即O(n)
样例输入:
10
4 5 6 9 8 7 1 2 3 0
5
样例输出:
9
8
7
6
5
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[100010];
void swap(int & a,int & b)
{
int tmp = a;
a = b;
b = tmp;
}
void FindMaxK(int a[],int s,int e,int k)
{
if( s - e +1 == k )
return;
int key = a[s]; //注意key值不是k值
int i = s,j = e;
while( i != j ) {
while( j > i && a[j] >= key )
--j; //注意符号在前
swap(a[i],a[j]);
while( i < j && a[i] <= key )
++i; //注意符号在前
swap(a[i],a[j]);
}
if( e-i+1 == k)
return;
else if( e-i+1 > k)
FindMaxK(a,i+1,e,k);
else
FindMaxK(a,s,i-1,k-e+i-1);
}
int main()
{
int n;
scanf("%d",&n);
for(int i = 0;i < n; ++i)
scanf("%d",&a[i]);
int k;
scanf("%d",&k);
FindMaxK(a,0,n-1,k);
//for(int i = 0;i < n; ++i)
// cout << a[i] << ",";
//cout << endl;
sort(a+n-k-1,a+n);
for(int i = n-1;i >= n-k; --i) //注意打印内容从n-k开始
printf("%d\n",a[i]);
return 0;
}
总结:
- 语法问题
1)sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp)
进行排序的时间复杂度为n*log2n
first:排序数组的起始地址
last:结束地址(最后一个数据的后一个数据的地址)
comp:升序或降序,如果不写,默认从小到大排序 - 逻辑问题
1)虽然但是!这题看似不难然而偶套快速排序还是没套好。。。
注意这道题和快速排序题最大的区别是这道题只需打印出前k个最大的数,所以不用完全排序。
如果第一遍分类结束,右边最大值刚好k个,直接输出;
如果大于k个,再对右边排序;
如果小于k个,再对左边排序,取最大的k-e+i-1个值
课后练习2:求排列的逆序数
描述:
在Internet上的搜索引擎经常需要对信息进行比较,比如可以通过某个人对一些事物的排名来估计他(或她)对各种不同信息的兴趣,从而实现个性化的服务。
对于不同的排名结果可以用逆序来评价它们之间的差异。考虑1,2,…,n的排列i1,i2,…,in,如果其中存在j,k,满足 j < k 且 ij > ik, 那么就称(ij,ik)是这个排列的一个逆序。
一个排列含有逆序的个数称为这个排列的逆序数。例如排列 263451 含有8个逆序(2,1),(6,3),(6,4),(6,5),(6,1),(3,1),(4,1),(5,1),因此该排列的逆序数就是8。显然,由1,2,…,n 构成的所有n!个排列中,最小的逆序数是0,对应的排列就是1,2,…,n;最大的逆序数是n(n-1)/2,对应的排列就是n,(n-1),…,2,1。逆序数越大的排列与原始排列的差异度就越大。
现给定1,2,…,n的一个排列,求它的逆序数。
输入:
第一行是一个整数n,表示该排列有n个数(n <= 100000)。
第二行是n个不同的正整数,之间以空格隔开,表示该排列。
输出:
输出该排列的逆序数。
解题思路:
解题思路
笨办法:O(n^2)
分治:O(nlogn)
1)将数组分为两半,分别求出左半边的逆序数和右半边的逆序数
2)再算有多少逆序是由左半边取一个数和右半边取一个数构成(要求O(n)实现)
关键: 左半边和右半边都是排好序的。比如,都是从大到小排序的。这样,左右半边只需要从头到尾各扫一遍,就可以找出由两边各取一个数构成的逆序个数
总结:由归并排序改进得到,加上计算逆序的步骤
样例输入:
6
2 6 3 4 5 1
样例输出:
8
提示:
提示
- 利用二分归并排序算法(分治);
- 注意结果可能超过int的范围,需要用long long存储。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[100010];
int b[100010];
void Merge(int a[],int s,int m, int e,int tmp[])
{// 对数组进行排序
int pb = 0;
int p1 = s,p2 = m+1;
/*cout << "merge1" << endl;
for(int i = s; i <= e; i++)
cout << a[i] << ",";
cout << endl;*/
while( p1 <= m && p2 <= e) {
if( a[p1] > a[p2])
tmp[pb++] = a[p1++];
else
tmp[pb++] = a[p2++];
}
while( p1 <= m)
tmp[pb++] = a[p1++];
while( p2 <= e)
tmp[pb++] = a[p2++];
for(int i = 0;i < e-s+1; ++i){
a[s+i] = tmp[i];
}
/*cout << "merge2" << endl;
for(int i = 0; i <= 5; i++)
cout << a[i] << ",";
cout << endl;*/
}
long long Count(int a[],int s,int m, int e)
{// 计算逆序值
long long result = 0;
int p1 = s,p2 = m+1;
/*cout << "count1" << endl;
for(int i = s; i <= e; i++)
cout << a[i] << ",";
cout << endl;*/
while( p1 <= m && p2 <= e) {
if( a[p1] > a[p2]) {
result += e-p2+1; //如果a[p1]>a[p2],则必然大于p2后面所有值
++p1;
}
else
++p2;
}
/*cout << "count2" << endl;
for(int i = s; i <= 5; i++)
cout << a[i] << ",";
cout << endl;*/
return result;
}
long long MergeSort(int a[],int s,int e,int tmp[])
{// 总体方案
long long result = 0;
if( s < e) {
int m = s + (e-s)/2;
result += MergeSort(a,s,m,tmp); //分别求两边的逆序数
//cout<<"左边 result="<<result << endl;
result += MergeSort(a,m+1,e,tmp);
//cout<<"右边 result="<<result << endl;
result += Count(a,s,m,e); //然后再o(n)算左边和右边造成的逆序数 。此时要求左边和右边都是从大到小有序的,才能在o(n)时间内算出结果
//cout<<"左右 result="<< result << endl;
Merge(a,s,m,e,tmp); //从大到小合并,确保排序
}
return result;
}
int main()
{
int n;
scanf("%d",&n);
for(int i = 0;i < n; ++i)
scanf("%d",&a[i]);
printf("%lld",MergeSort(a,0,n-1,b));
return 0;
}
/*
6
2 6 3 4 5 1
左边 result=0
右边 result=0
count1
2,6,
count2
2,6,3,4,5,1,
左右 result=0
merge1
2,6,
merge2
6,2,3,4,5,1,
左边 result=0
右边 result=0
count1
6,2,3,
count2
6,2,3,4,5,1,
左右 result=1
merge1
6,2,3,
merge2
6,3,2,4,5,1,
左边 result=1
左边 result=0
右边 result=0
count1
4,5,
count2
4,5,1,
左右 result=0
merge1
4,5,
merge2
6,3,2,5,4,1,
左边 result=0
右边 result=0
count1
5,4,1,
count2
5,4,1,
左右 result=2
merge1
5,4,1,
merge2
6,3,2,5,4,1,
右边 result=3
count1
6,3,2,5,4,1,
count2
6,3,2,5,4,1,
左右 result=8
merge1
6,3,2,5,4,1,
merge2
6,5,4,3,2,1,
8
*/
总结:
- 逻辑问题
1)这道题花了好久去做。。为毛?因为就是想不懂为啥它直接排序难道不需要记住原来的下标嘛,不然怎么知道是逆序?想不通。。把重要结果打印了一遍发现:
原数组:2 6 3 4 5 1
即先用count计算2,6的逆序数,然后用merge给2,6排序,当作一个整体再和3比较
即 count:2|6 result=0 merge: 2 6——> 6 2 count: 6 2|3 result=1
然后2 6 3计算结束后,进行后半部分 4 5 1的计算同上
最后进行6 3 2|5 4 1计算
其实方法思路里已经写的很明确了,但是偶递归学的太差勒。。。。