第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章 递归与分治策略相关推荐

  1. (算法设计与分析)第二章递归与分治策略-第二节:分治和典型分治问题

    文章目录 一:分治法基本概念 (1)基本思想 (2)适用条件 (3)复杂度分析 二:典型分治问题 (1)二分搜索 (2)大整数乘法 A:大整数乘法(Karatsuba算法) B:字符串乘法 (3)St ...

  2. 计算机算法设计与分析第五章思维导图知识点总结 ( 初稿 )

    复习链接 计算机算法设计与分析第一章思维导图 计算机算法设计与分析第二章思维导图&&知识点总结 计算机算法设计与分析第三章思维导图&&知识点总结 计算机算法设计与分析第 ...

  3. 算法设计与分析第七章分支限界算法(完结篇)

    算法设计与分析第七章分支限界算法 一.分支界限算法概述 1.分支限界法类似于回溯法,是一种在问题的解空间树上搜索问题解的算法. 分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解 ...

  4. 算法设计与分析第5章 回溯法(二)【回溯法应用】

    第5章 回溯法 5.2 应用范例 1.0-1背包问题 有n件物品和一个容量为c的背包.第i件物品的重量是w[i],价值是p[i].求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和 ...

  5. [XJTUSE 算法设计与分析] 第五章 回溯法

    第五章 回溯法 填空题会有代码填空,大题会手动回溯 学习要点 理解回溯法的深度优先搜索策略. 掌握用回溯法解题的算法框架 (1)递归回溯 (2)迭代回溯 (3)子集树算法框架 (4)排列树算法框架 5 ...

  6. 算法设计与分析之循环与递归

    前言: 循环与递归可以说是算法设计中最基本但却也是最重要的工具方法.循环和递归对于学习过高级程序设计语言的人来说都并不陌生,但还是有必要仔细的探究一下循环和递归之间的相似和区别.循环与递归最大的相似之 ...

  7. 算法设计与分析基础 第一章谜题

    习题1.1 10.b 欧几里得游戏 一开始,板上写有两个不相等的正整数,两个玩家交替写数字,每一次,当前玩家都必须在板上写出任意两个板上数字的差,而且这两个数字必须是新的,也就是说,不能与板上任何一个 ...

  8. 算法设计与分析第5章 回溯法(一)【回溯法】

    第5章 回溯法 5.1 回溯法 1.回溯法的提出  有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法. 2. 问题的解空间 (1)问题的解向量:回溯法希望 ...

  9. 算法设计与分析第4章 动态规划(一)【背包问题】

    第3章动态规划(一)[背包问题] 基本思想: 动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,但是经分解得到的子问题往往不是互相独立的.不同子问题的数目常常只有多项式量级.在用 ...

最新文章

  1. 某内存池中的指针用法
  2. 机器学习的门槛再度降低,AI小白如何用5分钟搞定建模?
  3. Linux Kernel TCP/IP Stack — L2 Layer — Linux VLAN device for 802.1.q(虚拟局域网)
  4. linux程序多少位,查看linux版本是多少位
  5. windows10完全删除mysql_Windows 10系统下彻底删除卸载MySQL的方法教程
  6. Context与ApplicationContext
  7. Centos 查看 登录 登出 重启 日志
  8. 分形吧matlab,Matlab在分形模拟上的一些应用
  9. 家用计算机的使用说明,AWIND奇机家用无线投屏器使用说明
  10. 三维可视化常见的技术路线浅析
  11. 马尔科夫随机场Markov Random Field
  12. 算法产品化---在ArmNN上运行ONNX
  13. MyBatis从入门到精通(1):MyBatis入门
  14. 函数与导数部分的题型梳理【中阶和高阶辅导】
  15. 医疗知识图谱问答系统(python neo4j)
  16. [转]sessionStorage在同一网站多个标签页内共享数据吗?这取决于标签页如何打开
  17. java字符转成ncr_用Java打印所有可能的nCr组合
  18. 大数据开发:Spark入门详解
  19. heidisql linux 安装_CentOS mysql GUI图形化数据库管理工具Heidisql的安装Navicat
  20. 20161221windows的snmp端口号更改

热门文章

  1. debian10 raid5+lvm
  2. 时间处理_pandas_时间处理小结
  3. LeetCode简单题之数组的度
  4. 王道考研 计算机网络笔记 第一章:概述计算机网络体系结构
  5. 芯片初创公司一亿融资可以烧多久
  6. TVMNN编译Compiler栈
  7. 操作系统常用词典(三)
  8. 前后端分离nginx配置,同时解决跨域问题
  9. Android TextView设置透明度方法的注意点
  10. TabLayout 在宽屏幕上tab不能平均分配的问题解决