分治就是将问题的一个实例划分为两个或更多个较小的实例(这些较小的实例通常也是原问题的实例),一直持续划分实例,直至问题规模小到可以轻松获得答案为止。实际上这也可以说是一种递归的思想。
二分查找(所查找的序列必须有序,查找时间复杂度O(logn) )
在包含size个元素的、从小到大排序的int数组a里查找元素p,如果找到,则返回元素下标,如果找不到,则返回-1。
二分查找函数非递归版本:
int BinarySearch(int a[],int size,int p)
{
int L=0;
int R=size-1;
int mid;
while(L<=R)
{
mid=L+(R-L)/2;
if(p==a[mid]) return mid;
else if(p<a[mid])
{
R=mid-1;
}
else
{
L=mid+1;
}
}
return -1;//若没找到则返回-1
}
二分查找函数递归版本:
int rBinarySearch(int a[],int l,int r,int x)
{
if(l>r) return -1;
else
{
int mid=l+(r-l)/2;
if(x==a[mid]) return mid;
else if(x<a[mid]) return rBinarySearch(a,l,mid-1,x);
else return rBinarySearch(a,mid+1,r,x);
}
}
二分查找例题
寻找指定和的整数对
输入n ( n<= 100,000)个整数, 找出其中的两个数,它们之和等于整数m(假定
肯定有解)。 题中所有整数都能用 int 表示
解法1:
- 将数组排序,复杂度是O(n×log(n))
- 对数组中的每个元素a[i],在数组中二分查找m-a[i],看能否找到。复杂度log(n),最
坏要查找n-2次,所以查找这部分的复杂度也是O(n×log(n))
这种解法总的复杂度是O(n×log(n))的。
解法2:
- 将数组排序,复杂度是O(n×log(n))
- 查找的时候,设置两个变量i和j,i初值是0,j初值是n-1.看a[i]+a[j],如果大于m,就让j
减1,如果小于m,就让i加1,直至a[i]+a[j]=m。
这种解法总的复杂度是O(n×log(n))的。
归并排序
数组排序任务可以如下完成:
- 把前一半排序
- 把后一半排序
- 把两半归并到一个新的有序数组,然后再拷贝回原数组,排序完成。
#include <iostream>
using namespace std;
void Merge(int a[],int s,int m, int e)//将两部分合并
{
int p1=s,p2=m+1;
int pb=0;
int *tmp=new int[e-s+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];
delete [] tmp;
}
void MergeSort(int a[],int s,int e)//递归实现合并排序
{
if( s < e)
{
int m = s + (e-s)/2;
MergeSort(a,s,m);
MergeSort(a,m+1,e);
Merge(a,s,m,e);
}
}
快速排序
数组排序任务可以如下完成:
- 设k=a[0], 将k挪到适当位置,使得比k小的元素都在k左边,比k大的元素都在k右边,和k相等的在k左右出现均可 ( O( n)时间完成)
- 把k左边的部分快速排序
- 把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) return;
int k = a[s];
int i = s,j = e;
while( i != j )
{
while( j > i && a[j] >= k ) --j;
swap(a[i],a[j]);
while( i < j && a[i] <= k ) ++i;
swap(a[i],a[j]);
} //处理完后, a[i] = k
QuickSort(a,s,i-1);
QuickSort(a,i+1,e);
}
例题:输出前m大的数
描述:给定一个数组包含n个元素,统计前m大的数并且把这m个数从大到小输出。
输入
第一行包含一个整数n,表示数组的大小。 n < 100000。
第二行包含n个整数,表示数组的元素,整数之间以一个空格分开。每个整数的绝对值不超过100000000。
第三行包含一个整数m。 m < n。
输出
从大到小输出前m大的数,每个数一行。
解法一:排序后再输出,复杂度 O(nlogn)
解法二:用分治处理: 复杂度 O(n+mlogm)
思路:把前m大的都弄到数组最右边,然后对这最右边m个元素排序,再输出
关键 : O(n)时间内实现把前m大的都弄到数组最右边
引入操作 arrangeRight(k): 把数组(或数组的一部分)前k大的都弄到最右边
如何将前k大的都弄到最右边
1)设key=a[0], 将key挪到适当位置,使得比key小的元素都在
key左边,比key大的元素都在key右边(线性时间完成)
2) 选择数组的前部或后部再进行 arrangeRight操作
例题:快速幂
非递归实现:
int Pow(int a,int b)
{ //快速求a^b ,复杂度 log(b)
int result = 1;
int base = a;
while(b)
{
if( b & 1)
result *= base;
base *= base;
b >>= 1;
}
return result;
}
递归实现:
int Pow(int a,int b)
{ //快速求a^b ,复杂度 log(b)
if(b == 0)
return 1;
if(b & 1) //b是奇数
{
return a * Pow(a,b-1);
}
else
{
int t = Pow(a,b/2);
return t * t;
}
}
不应使用分治方法的情况:
- 一个规模为n的实例被划分成两个或多个实例,而每个实例的复杂度仍然几乎为n
- 一个规模为n的实例被划分为差不多n个规模为n/c的实例(c为常数)
比如很多书都会举的斐波那契数列,如使用分治(递归)的方法将其分为f(n-1)与f(n-2)就会导致大量的重复计算,上述两种情况的本质就是如此,只是导致的复杂度规模有所不同,此处不详谈。
而解决此类问题,一般会使用循环或者动态规划。