基于递归思想,将大问题拆解为小问题,再由小问题的解构造原问题的解
- 设计思路:分三步执行 ------ 分解问题为同类子问题、独立求解各子问题、合并子问题的解得到原问题答案,通常结合递归实现。
- 适用场合:适用于可拆解为同类子问题,且子问题的解能合并为原问题解的场景。
- 注意事项
- 子问题需相互独立
- 子问题数量要满足递归求解条件
- 保证子问题解合并的正确性
- 优化递归深度与重复计算,提升算法效率
【例题1】快速排序
快速排序:选择一个基准元素,将数组分成两部分,使得:
左边所有元素 ≤ 基准
右边所有元素 ≥ 基准
然后递归地对左右两部分排序
分区过程详解(挖坑法):
1.选基准key=a[s],留下"坑"hole=s
2.从右向左找第一个≤key的元素,填入左边的坑
3.从左向右找第一个≥key的元素,填入右边的坑
4.重复直到左右指针相遇
5.将基准放入最后的位置
cpp
#include<iostream>
using namespace std;
int Partition(int a[],int s,int e){
int key=a[s]; // 选择第一个元素作为基准
int L=s,R=e; // L:左指针,R:右指针
int hole=s; // 空位索引(挖坑法)
while(L<R){
while(L<R && a[R]>key){ // 从右向左找第一个≤key的元素
R--;
}
a[hole]=a[R]; // 将找到的元素填到空位
hole=R; // 更新空位位置
while(L<R && a[L]<key){ // 从左向右找第一个≥key的元素
L++;
}
a[hole]=a[L]; // 将找到的元素填到空位
hole=L; // 更新空位位置
}
a[hole]=key; // 基准放入最终位置
return L; // 返回基准的最终位置
}
void sort(int a[],int s,int e){ //s=start,e=end
if(s>=e) return; // 递归出口:区间为空或只有一个元素
// 分区,得到基准位置
int i=Partition(a,s,e);
//递归解决子问题
sort(a,s,i-1); // 排序基准左边的子数组
sort(a,i+1,e); // 排序基准右边的子数组
}
int main(){
int a[]={2,5,8,3,6,1,4,7,9};
//数组长度
int n=sizeof(a)/sizeof(a[0]);
sort(a,0,n-1); // 对[0..8]排序
for(int i=0;i<n;i++){
cout<<a[i]; // 输出排序结果
}
return 0;
}
【例题2】归并排序
自顶向下的二路归并排序算法(代码相对自底向上简单)
归并排序:将数组不断二分,直到每个子数组只有一个元素,然后将这些有序子数组合并成更大的有序数组。
三步走:
1.分解:将数组分成两半
2.解决:递归排序两个子数组
3.合并:合并两个已排序的子数组
cpp
#include<iostream>
using namespace std;
void merge(int a[],int s,int mid,int e){
int i=s, j=mid+1; // i:左子数组起始,j:右子数组起始
int temp[e-s+1], k=0; // 临时数组存放合并结果
// 比较左右子数组,取较小元素放入temp
while(i<=mid&&j<=e){
if(a[i]<a[j]){
temp[k++]=a[i++];
}else{
temp[k++]=a[j++];
}
}
// 处理剩余元素(这两个循环只会执行一个)
while(i<=mid) temp[k++]=a[i++];
while(j<=e) temp[k++]=a[j++];
// 将临时数组的值还回a数组中
for(int l=0,r=s;r<=e;l++,r++){
a[r]=temp[l];
}
}
void mergesort(int a[],int s,int e){
if(s >= e) return; // 递归出口:区间只有一个或零个元素
int mid = (s+e)/2; // 计算中点,分解:mid = (s+e)/2将数组分成两半
mergesort(a,s,mid); // 递归排序左半部分
mergesort(a,mid+1,e); // 递归排序右半部分
merge(a,s,mid,e); // 合并两个有序子数组
}
int main(){
int a[]={2,5,1,7,10,6,9,4,3,8};
int n=sizeof(a)/sizeof(a[0]);
mergesort(a,0,n-1); // 对整个数组排序
for(int i=0;i<n;i++){
cout<<a[i];
}
return 0;
}
【例题3】查找最大和次大元素
问题描述:对于给定的含有n元素的无序序列,求这个序列中最大和次大的两个不同的元素
例如:(2,5,1,4,6,3),最大的元素为6,次大元素为5
a[]:输入数组
s:查找区间的起始索引
e:查找区间的结束索引
max1, max2:引用参数,返回该区间的最大值和第二大值
cpp
#include<iostream>
using namespace std;
void find(int a[],int s,int e,int &max1,int &max2){
//递归出口
if(s==e){ // 只有一个元素
max1=a[s];
max2=-1; // 没有第二大值,用-1表示
}else if(s+1==e){ // 只有两个元素
//直接比较两个元素,大的为max1,小的为max2
if(a[s]>=a[e]){
max1=a[s];
max2=a[e];
}else{
max1=a[e];
max2=a[s];
}
}else{ // 超过两个元素
int mid=(s+e)/2;
int lmax1,lmax2,rmax1,rmax2;
// 递归解决左半部分
find(a,s,mid,lmax1,lmax2);
// 递归解决右半部分
find(a,mid+1,e,rmax1,rmax2);
//小问题的解合并为原问题的解,合并结果
if(lmax1 > rmax1){ // 如果 lmax1 > rmax1,则全局最大是 lmax1
max1 = lmax1;
max2 = (lmax2 > rmax1) ? lmax2 : rmax1; // 左边次大比右边最大
} else { // 否则全局最大是 rmax1
max1 = rmax1;
max2 = (rmax2 > lmax1) ? rmax2 : lmax1; // 右边次大比左边最大
}
}
}
int main(){
int a[]={2,5,1,4,6,3};
int n=sizeof(a)/sizeof(a[0]);
int max1,max2;
find(a,0,n-1,max1,max2);
cout<<max1<<","<<max2;
return 0;
}
【例题4】折半查找(二分查找)
a[]:已排序的数组(必须有序)
l:查找范围的左边界索引
r:查找范围的右边界索引
num:要查找的目标值
cpp
#include<iostream>
using namespace std;
int binsearch(int a[],int l,int r,int num){ // 二分查找函数
int mid;
while(l<r){ // 只要左边界 l小于右边界 r,就继续查找
mid = (l+r)/2;
if(a[mid]==num) return mid; // 找到目标
else if(a[mid]<num) l=mid+1; // 目标在右侧
else if(a[mid]>num) r=mid-1; // 目标在左侧
}
return -1; // 返回值:找到则返回索引,未找到返回 -1
}
int main(){
int a[]={1,2,3,4,5,6,7,8,9};
int n=sizeof(a)/sizeof(a[0]);
int index = binsearch(a,0,n-1,8); //查找数字为8
cout<<index;
return 0;
}
扩展:
查找数字第一次出现的位置
cpp
#include<iostream>
using namespace std;
int binsearch(int a[],int l,int r,int num){
int mid;
while(l+1!=r){
mid = (l+r)/2;
if(a[mid]<num) l=mid;
else if(a[mid]>=num) r=mid;
}
return r;
}
int main(){
int a[]={1,2,3,4,5,6,7,8,8,8,8,8,8,8,9};
int n=sizeof(a)/sizeof(a[0]);
int index = binsearch(a,0,n-1,8);
cout<<index;
return 0;
}
查找数字最后一次出现的位置
cpp
#include<iostream>
using namespace std;
int binsearch(int a[],int l,int r,int num){
int mid;
while(l+1!=r){
mid = (l+r)/2;
if(a[mid]<=num) l=mid;
else if(a[mid]>num) r=mid;
}
return l;
}
int main(){
int a[]={1,2,3,4,5,6,7,8,8,8,8,8,8,8,9};
int n=sizeof(a)/sizeof(a[0]);
int index = binsearch(a,0,n-1,8);
cout<<index;
return 0;
}
注意:没有重复的也可以查找到相应的数字,但推荐使用第一个代码进行二分查找
【例题5】求解循环日程安排问题
设有n=2^k个选手要进行网球循环赛,要求设计一个满足以下要求的比赛日程表(分治法)
1.每个选手必须与其他n-1个选手各赛一次
2.每个选手一天只能赛一次
3.循环赛在n-1天之内结束
cpp
#include<iostream>
using namespace std;
int k=4; // 2^k 个选手,这里k=4表示16个选手
int a[101][101]; // 日程表,a[i][j]表示选手i在第j天的对手
void plan(int k){
a[0][0]=1 ,a[0][1]=2; // 选手1:编号1,第1天对手是2
a[1][0]=2 ,a[1][1]=1; // 选手2:编号2,第1天对手是1
//构造复杂问题
int n=2; // 从2个选手开始
for(int s=1;s<=k;s++){ // s表示当前扩展次数
int temp=n; // 保存当前规模
n=n*2; // 规模翻倍
//构造左下角
for(int i=temp;i<n;i++){
for(int j=0;j<temp;j++){
a[i][j] = a[i-temp][j] + temp;
}
}
//构造右下角
for(int i=temp;i<n;i++){
for(int j=temp;j<n;j++){
a[i][j] = a[i-temp][j-temp];
}
}
//构造右上角
for(int i=0;i<temp;i++){
for(int j=temp;j<n;j++){
a[i][j] = a[i][j-temp]+temp;
}
}
}
}
int main(){
plan(k);
for(int i=0;i<1<<k;i++){
for(int j=0;j<1<<k;j++){
cout<<a[i][j]<< " ";
}
cout<<endl;
}
return 0;
}