减治法
概述
减治法把一个大问题划分为若干个子问题,但是只需求解其中的一个子问题,也无需对子问题的解进行合并。所以,严格地说,减治法应该是一种退化了的分治法
求解过程
- 将问题分解为若干个子问题
-
- 原问题的解只存在于其中一个较小规模的子问题中;
- 原问题的解与其中一个较小规模的解之间存在某种确定的对应关系
例题
折半查找
实现
int BinarySearch(int a[],int n,int key){
int low=0,high=n-1,mid;
while(low<=high){
mid=(low+high)/2;
if(a[mid]==key) return mid;
else if(a[mid]>key) high=mid-1;
else low=mid+1;
}
return -1;
}
选择问题
问题
给定一个无序的数组,求第k小的元素
分析
类似快速排序的分治思想,每次选取一个基准元素,将数组分为两部分,左边的元素都小于基准元素,右边的元素都大于基准元素,如果基准元素的下标为i,那么第k小的元素就是第k小的元素
实现
int Partition(int a[],int low,int high){
int pivot=a[low];
while(low<high){
while(low<high&&a[high]>=pivot) high--;
a[low]=a[high];
while(low<high&&a[low]<=pivot) low++;
a[high]=a[low];
}
a[low]=pivot;
return low;
}
int Select(int a[],int low,int high,int k){
int pivotpos=Partition(a,low,high);
int m=pivotpos-low+1;
if(m==k) return a[pivotpos];
else if(m>k) return Select(a,low,pivotpos-1,k);
else return Select(a,pivotpos+1,high,k-m);
}
堆排序
实现
void ShitHeap(int r[],int k,int n){
int i = k;
int j = 2*i+1;
while(j<n){
if(j+1<n && r[j+1]>r[j]){
j++;//j指向左右孩子中较大的那个
}
if(r[j]>r[i]){
int temp = r[j];
r[j] = r[i];
r[i] = temp;
i = j;
j = 2*i+1;//继续向下筛选
}else{
break;
}
}
}
void BuildHeap(int r[],int n){
for(int i=n/2-1;i>=0;i--){
ShitHeap(r,i,n);
}
//排序
for(int i=n-1;i>=0;i--){
int temp = r[0];
r[0] = r[i];
r[i] = temp;
ShitHeap(r,0,i);
}
}
假币问题
问题
在 n 枚外观相同的硬币中,有一枚是假币,并且已知假币较轻。通过一架来任意比较两组硬币,从而得知两组硬币的重量是否相同,或者哪一组更轻一些,假币问题要求设计一个高效的算法来检测出这枚假币
分析
将硬币分为三组,每组 n/3(向上取整) 枚,如果两组硬币的重量相同,那么假币就在第三组,否则假币就在重量较轻的那一组。然后再将这一组硬币分为三组,重复上述过程,直到找到假币
实现
int coin(int c[],int low,int high,int n){
int num1=0,num2=0,num3=0;
if (n==1) return low+1;
if (n%3==0)
num1=num2=n/3;
else
num1=num2=n/3+1;
num3=n-num1-num2;
//第一组重量
int w1=0;
for (int i=0;i<num1;i++)
w1+=c[low+i];
//第二组重量
int w2=0;
for (int i=0;i<num2;i++)
w2+=c[low+num1+i];
//第三组重量
int w3=0;
for (int i=0;i<num3;i++)
w3+=c[low+num1+num2+i];
if (w1<w2)
return coin(c,low,num1,n/3);
else if (w1>w2)
return coin(c,low+num1,num2,n/3);
else
return coin(c,low+num1+num2,num3,n/3);
}
两个序列的中位数
问题
给定两个等长的有序数组 A 和 B,找到两个数组的中位数
分析
分别求两个数组中位数
- 两个数组的中位数相等,既中位数
- 中位数一定在两个数组的中位数的左边或者右边
- 当两个数组均为1时,中位数为两个数组中位数的较小值
实现
int mid(int a[],int b[],int n){
int s1=0,s2=0;
int e1=n-1,e2=n-1;
int m1,m2;
while(s1<e1&&s2<e2){
m1=(s1+e1)/2;
m2=(s2+e2)/2;
if(a[m1]==b[m2])
return a[m1];
if (a[m1]>b[m2]){
//保留a中m1之前的元素和b中m2之后的元素
if((s1+e1)%2==0){
e1=m1;
s2=m2+1;
}
else{
e1=m1;
s2=m2;
}
}
else{
if((s2+e2)%2==0){
s1=m1+1;
e2=m2;
}
else{
s1=m1;
e2=m2;
}
}
return a[s1]<b[s2]?a[s1]:b[s2];
}
}
topK问题
问题
从大批量数据中找出前K大的数据
分析
创建k个大小的小顶堆,遍历数据,如果数据大于堆顶元素,就替换堆顶元素,然后调整堆
实现
int maxTopK(int a[],int n,int k,int max[]){
//用前k个值构建小根堆
for(int i=0;i<k;i++){
max[i]=a[i];
for(int j=i;j>0;){
if(max[j]<max[(j-1)/2]){
int temp=max[j];
max[j]=max[(j-1)/2];
max[(j-1)/2]=temp;
j=(j-1)/2;
}
else
break;
}
}
//遍历剩余元素,若大于堆顶元素,则替换堆顶元素
for(int i=k;i<n;i++){
if(a[i]>max[0]){
max[0]=a[i];
for(int j=0;j<k;){
if(max[j]>max[2*j+1]||max[j]>max[2*j+2]){
if(max[2*j+1]<max[2*j+2]){
int temp=max[j];
max[j]=max[2*j+1];
max[2*j+1]=temp;
j=2*j+1;
}
else{
int temp=max[j];
max[j]=max[2*j+2];
max[2*j+2]=temp;
j=2*j+2;
}
}
else
break;
}
}
}
}
练习
-
- 问题:折半查找范围
- 分析:折半查找上限索引和下限索引
- 实现:
void findBetween(int a[],int n,int x,int y,int &i,int &j){ //找上限 int mid,low=0,high=n-1; while(low<=high){ mid=(low+high)/2; if(a[mid]>x) high=mid-1; else if(a[mid]==x) {low=mid;break;} else low=mid+1; } i=low; //找下限 low=0,high=n-1; while(low<=high){ mid=(low+high)/2; if(a[mid]<y) low=mid+1; else if(a[mid]==y) {high=mid;break;} else high=mid-1; } j=high; }