第2章 递归与分治策略
2.1 递归算法
递归算法:直接或间接地调用自身的算法。
递归函数:用函数自身给出定义的函数。两个要素:边界条件、递归方程
优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性。
缺点:运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。
解决方法:在递归算法中消除递归调用,使其转化为非递归算法。
1、采用一个用户定义的栈来模拟系统的递归调用工作栈。该方法通用性强,但本质上还是递归,只不过人工做了本来由编译器做的事情,优化效果不明显。
2、用递推来实现递归函数。
3、通过变换能将一些递归转化为尾递归,从而迭代求出结果。
后两种方法在时空复杂度上均有较大改善,但其适用范围有限。
例1.阶乘函数
代码:
#include<iostream>#include<cstdio>#include<cstring>#include<cmath>#include<string>#include<stdlib.h>#include<algorithm>using namespace std;long long factorial(long long n){if(n==0) return 1;else return n*factorial(n-1);}int main(){long long n;long long result;cout<<"this app compute the factorial of n,please input n:"<<endl;while(cin>>n){result=factorial(n);cout<<"The factorial of "<<n<<" is: "<<result<<endl;}return 0;}
例2. Fibonacci数列
无穷数列1,1,2,3,5,8,13,21,34,55,……,称为Fibonacci数列。它可以递归地定义为:
代码:
#include<iostream>#include<stdlib.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;int Fibonacci(int n){if(n==0||n==1) return 1;else return Fibonacci(n-1)+Fibonacci(n-2);}int main(){int n,result;cout<<"this app compute Fibonacci of n, please input n:"<<endl;while(cin>>n){result=Fibonacci(n);cout<<"the Fibonacci of "<<n<<" is :"<<result<<endl;}return 0;}
例3 .Ackerman函数
当一个函数及它的一个变量是由函数自身定义时,称这个函数是双递归函数。
Ackerman函数A(n,m)定义如下:
代码:
#include<iostream>#include<stdlib.h>#include<math.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;int Ackerman(int n,int m){if(n==1&&m==0) return 2;else if(n==0&&m>=0) return 1;else if(n>=2&&m==0) return n+2;else if(n>=1&&m>=1) return Ackerman(Ackerman(n-1,m),m-1);}int main(){int n,m,result;cout<<"this app compute the problem of Ackerman,please input n and m:"<<endl;while(cin>>n>>m){result=Ackerman(n,m);cout<<"the result is :"<<result<<endl;}return 0;}
例4.全排列问题:123 -> 123 132 213 231 312 321
分析:可以采取以下步骤:
(1)数组的第一个元素为1(排列的第一个元素为1),生成后面的n-1个排列;
数组的第一元素与第二元素互换,使得排列的第一个元素为2,生成后面的n-1个排列;
依次类推,最后数组第一元素与数组第n元素互换,使排列第一元素为n,生成后面的n-1个排列;
(2)上述第一步骤中,为生成后面的n-1个元素的排列,继续采取以下步骤:
数组的第二个元素为2(排列的第二个元素为2),生成后面的n-2个排列;
数组的第二元素与第三元素互换,使得排列的第一个元素为3,生成后面的n-2个排列;
依次类推,最后数组第二元素与数组第n元素互换,使排列第二元素为n,生成后面的n-2个排列;
(3)上述步骤持续进行,即当排列的前n-2个元素确定后,为生成后面的2个元素的排列,可按照前面类似方式进行:
数组的第n-1个元素为n-1(排列的第n-1个元素为n-1),生成后面的1个排列,此时数组中的n个元素已经构成了一个排列;
数组的第n-1元素与第n元素互换,使得排列的第n-1个元素为n,生成后面的1个元素的排列,此时数组中的n个元素已经构成了一个排列;
若排列算法pl_alm(A,k,n)表示生成数组后面的k个元素的排列,则通过上述分析,有:
基础步:k=1,只有一个元素,已经形成一个排列;
归纳步:对于任意一个k(k=2,3,…,n),若可根据算法pl_alm(A,k-1,n)完成数组后面k-1个元素的排列,则为了完成数组后面k个元素的排列pl_alm(A,k,n),应该逐一对数组中的第n-k元素与数组中的n-k~n元素进行互换,每互换一次,就执行一次pl_alm(A,k-1,n)操作,并进而产生一个排列。
算法的时间复杂度为: O(n*n!)
代码:
#include<iostream>#include<stdlib.h>#include<math.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;void pl_alm(int A[],int k,int n){int i;if(k==1){for(i=0; i<n; i++){cout<<A[i];}cout<<endl;}else{for(i=n-k; i<n; i++) //循环将当前位置元素依次与其后元素互换{swap(A[n-k],A[i]);pl_alm(A,k-1,n); //递归生成后面的k-1个元素的全排列swap(A[n-k],A[i]);}}}int main(){int A[52];int i,n;cout<<"please input the elements sum of A(must less than 50): "<<endl;cin>>n;if(n>50){cout<<"out of range! finished!"<<endl;return 0;}cout<<"please input all of elements of A:"<<endl;for(i=0; i<n; i++){cin>>A[i];}pl_alm(A,n,n);return 0;}
例5.整数划分问题
将正整数n表示成一系列正整数之和:n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1,k≥1。正整数n的这种表示称为正整数n的划分。求正整数n的不同划分个数。
分析:
如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量:将最大加数n1不大于m的划分个数记作q(n,m),可以建立q(n,m)的如下递归关系:
(1) q(n,1)=1,n>=1;
当最大加数n1不大于1时,任何正整数n只有一种划分形式,即n=1+1+1+…+1
(2) q(n,m)=q(n,n),m>=n;
最大加数n1实际上不能大于n,因此,q(1,m)=1。
(3) q(n,n)=1+q(n,n-1);
正整数n的划分由n1=n的划分和n1≤n-1的划分组成。
(4) q(n,m)=q(n,m-1)+q(n-m,m),n>m>1;
正整数n的最大加数n1不大于m的划分由n1=m的划分和
n1≤m-1 的划分组成。
例如正整数6有如下11种不同的划分:
6;
5+1;
4+2,4+1+1;
3+3,3+2+1,3+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
1+1+1+1+1+1。
分析如下:
q(6,6)=1+q(6,5)
q(6,5)=q(6,4)+q(1,5)=q(6,4)+1
q(6,4)=q(6,3)+q(2,4)=q(6,3)+q(2,2)=q(6,3)+q(2,1)+1=q(6,3)+1+1
q(6,3)=q(6,2)+q(3,3)=q(6,2)+q(3,2)+1=q(6,2)+q(3,1)+q(1,2)+1=q(6,2)+1+1+1
q(6,2)=q(6,1)+q(4,2)=q(6,1)+q(4,1)+q(2,2)=q(6,1)+1+q(2,1)+1=q(6,1)+1+1+1
代码:
#include<iostream>#include<stdlib.h>#include<math.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;int division(int n,int m){if(n==1||m==1) return 1;else if(n<m) return division(n,n);else if(n==m) return division(n,n-1)+1;else if(n>m) return division(n,m-1)+division(n-m,m);}int main(){int n,num;cout<<"this app divide n,please input n:"<<endl;cin>>n;num=division(n,n);cout<<"the number of the division of n is: "<<num<<endl;return 0;}
例6. Hanoi塔问题
设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:
规则1:每次只能移动1个圆盘;
规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
规则3:在满足移动规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。
代码:
#include<iostream>#include<cstdio>#include<cstring>#include<cmath>#include<stdlib.h>#include<string>#include<algorithm>using namespace std;int k;void dispmove(int n,char x,char y){cout<<"NO."<<++k<<": ";cout<<"floor."<<n<<":"<<x<<"->"<<y<<endl;}void hanoi(int n,char A,char B,char C){if(n>0){hanoi(n-1,A,C,B);dispmove(n,A,C);hanoi(n-1,B,A,C);}}int main(){int n;cout<<"请输入汉诺塔层数:"<<endl;while(cin>>n){k=0;hanoi(n,'A','B','C');cout<<"The process is finished!"<<endl;}return 0;}
例7.秦九韶算法
一般地,一元n次多项式的求值需要经过2n-1次乘法和n次加法,而秦九韶算法只需要n次乘法和n次加法。
一次多项式:
改写如下形式:
时间复杂度:O(N),空间复杂度:O(N)
代码:
#include<iostream>#include<stdlib.h>#include<math.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;float qinjiushao_alm(float x,float A[],int n) //A[0...n],由于计算时倒序,即A[0]*pow(x,n)+A[1]*pow(x,n-1)+...+A[n-1]*x+A[n],所以请输入时按A[n...0]顺序输入{float p;if(n==0) p=A[0];else p=qinjiushao_alm(x,A,n-1)*x+A[n];return p;}int main(){int i,n;float x=0,result=0;float A[100];cout<<"please input order number of polynomial A:"<<endl;cin>>n;cout<<"please input coefficient of polynomial A:"<<endl;for(i=0; i<=n; i++){cin>>A[i];}cout<<"please input value of x:"<<endl;cin>>x;result = qinjiushao_alm(x,A,n);cout<<"the result of polynomial is:"<<result<<endl;return 0;}
2.2 分治法
基本条件:
1.原问题可分割成k个子问题,1<k≤n
2.这些子问题都可解
3.可利用这些子问题的解求出原问题的解
特征:
1.该问题的规模缩小到一定的程度就可以容易地解决;
2.该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质
(如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质)
3.利用该问题分解出的子问题的解可以合并为该问题的解;
4.该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
例1.二分搜索
给定已按升序排好序的n个元素a[0:n-1],现要在这n个元素中找出一特定元素x。
时间复杂度:O(logn)
代码:
#include<iostream>#include<stdlib.h>#include<math.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;int Binary_Search(int a[],int x,int n){int l=1,r=n;while(l<=r){int mid=(l+r)/2;if(x==a[mid]) return mid;else if (x>a[mid]) l=mid+1;else r=mid-1;}return -1;}int main(){int a[100];int i,n,x,pos;cin>>n;for(i=1; i<=n; i++){cin>>a[i];}while(cin>>x){pos=Binary_Search(a,x,n);if(pos==-1) cout<<"not found!"<<endl;else cout<<"the position of "<<x<<" is: "<<pos<<endl;//利用C++自带二分搜索函数可判断x是否存在/*if(binary_search(a+1,a+n+1,x))cout<<"the C++ function can find x!"<<endl;elsecout<<"the C++ function cannot find x!"<<endl;*/}return 0;}
例2.合并排序
基本思想:将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并成为所要求的排好序的集合。
时间复杂度:
T(n)=O(1),n<=1
T(n)=2T(n/2)+O(n),n>1
T(n)=O(nlogn)是渐进意义上的最优算法,辅助空间O(n)
性能分析:与其他O(NlogN)排序算法比较,归并排序的运行时间严重依赖于比较元素和在数组中移动元素的相对开销,这和语言有关。比如在Java中比较操作是昂贵的,但移动元素的开销的较小的;而C++通常相反。
合并排序比较占内存,但效率高且稳定。
基本步骤:
1.申请两个与已经排序序列相同大小的空间,并将两个序列拷贝其中;
2.设定最初位置分别为两个已经拷贝排序序列的起始位置,比较两个序列元素的大小,依次选择相对小的元素放到原始序列;
3.重复2直到某一拷贝序列全部放入原始序列,将另一个序列剩下的所有元素直接复制到原始序列尾。
设归并排序的当前区间是R[low…high],分治法的三个步骤是:
1.分解:将当前区间一分为二,即求分裂点
2.求解:递归地对两个子区间R[low…mid]和R[mid+1…high]进行归并排序;
3.组合:将已排序的两个子区间R[low…mid]和R[mid+1…high]归并为一个有序的区间R[low…high]。递归的终结条件:子区间长度为1(一个记录自然有序)。
代码:
#include<iostream>#include<stdlib.h>#include<math.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;//将两个有序子数组a[begin...mid]和a[mid+1...end]合并,(负责在合并时排序)void MergeArray(int a[],int begin,int mid,int end,int temp[]){int i=begin,j=mid+1;int m=mid,n=end;int k=0;while(i<=m&&j<=n){if(a[i]<=a[j]){temp[k++]=a[i++];}else{temp[k++]=a[j++];}}while(i<=m){temp[k++]=a[i++];}while(j<=n){temp[k++]=a[j++];}//把temp数组中的结果装回a数组for(i=0; i<k; i++){a[begin+i]=temp[i];}}void mergesort(int a[],int begin,int end,int temp[]) //区间分解{if(begin<end){int mid=(begin+end)/2;mergesort(a,begin,mid,temp); //左边有序mergesort(a,mid+1,end,temp); //右边有序MergeArray(a,begin,mid,end,temp); //将左右两边有序的数组合并}}int main(){int i,n;int a[110],temp[110];while(cin>>n){for(i=0; i<n; i++){cin>>a[i];}mergesort(a,0,n-1,temp);for(i=0; i<n-1; i++){cout<<a[i]<<" ";}cout<<a[n-1]<<endl;}return 0;}
例3.快速排序
在快速排序中,记录的比较和交换是从两端向中间进行的,关键字较大的记录一次就能交换到后面单元,关键字较小的记录一次就能交换到前面单元,记录每次移动的距离较大,因而总的比较和移动次数较少。
最坏情况时间复杂度为O(n*n),最好情况时间复杂度为O(nlogn),平均时间复杂度为O(nlogn),辅助空间O(n)或O(logn)
代码:
#include<iostream>#include<stdlib.h>#include<math.h>#include<cstdio>#include<string>#include<cstring>#include<cmath>#include<algorithm>using namespace std;int Partition(int a[],int p,int r) {int i=p,j=r+1;int x=a[p];//将<x的元素交换到左边区域//将>x的元素交换到右边区域while(true){while(a[++i]<x);while(a[--j]>x);if(i>=j) break;//cout<<"i="<<i<<",j="<<j<<",a[i]="<<a[i]<<",a[j]="<<a[j]<<endl;swap(a[i],a[j]);}a[p]=a[j];a[j]=x;//cout<<"a[p]="<<a[p]<<",a[j]="<<a[j]<<endl;return j;}void QuickSort(int a[],int p,int r){if(p<r){int q=Partition(a,p,r);QuickSort(a,p,q-1); //对左半段排序QuickSort(a,q+1,r); //对右半段排序}}int main(){int i,n;int a[110];while(cin>>n){for(i=0; i<n; i++){cin>>a[i];}QuickSort(a,0,n-1);for(i=0; i<n-1; i++){cout<<a[i]<<" ";}cout<<a[n-1]<<endl;}return 0;}
如果觉得《算法设计与分析第2章 递归与分治策略》对你有帮助,请点赞、收藏,并留下你的观点哦!