函数栈:后入先出。
函数调用关键概念:调用另一个函数x2时,当前函数x1暂停并处于未完成状态,此函数的所有变量(如变量a)的值都在内存中。当函数x2结束后,程序回到函数x1的代码,从调用x2行代码下一行开始执行,可以调用变量a的值。
递归例1:用递归函数求n!。 (按数学公式的写法写递归函数)
两种形式。
#include<bits/stdc++.h>
using namespace std;
int cnt1=0,cnt2=0;
long long factorial1(int n){
cnt1++;
if(n==1) return 1;
return n*factorial1(n-1);
}
long long factorial2(int n){
cnt2++;
if(n==1) return 1;
long long re=n;
re*=factorial2(n-1);
return re;
}
long long comp(int n){
long long re=1;
for(int i=n;i>=1;i--)
re*=i;
return re;
}
int main()
{
int n; cin>>n;
cout<<factorial1(n)<<endl;
cout<<cnt1<<endl; //函数递归执行次数
cout<<factorial2(n)<<endl;
cout<<cnt2<<endl; //函数递归执行次数
cout<<comp(n); //非递归算法
return 0;
}
输入20
结果为:
2432902008176640000
20
2432902008176640000
20
2432902008176640000
递归例2:用递归函数求斐波那契数列的第n项。 (记事本节省递归次数)
数学上斐波那契数列的第n项可以递归定义为:
#include <bits/stdc++.h>
using namespace std;
long long f[100];
int cnt1=0,cnt2=0;
long long Fibonacci1( int n){
cnt1++;
if (n<3) return 1;
return Fibonacci1(n-1)+Fibonacci1(n-2);
}
long long Fibonacci2( int n){ //记事本方法
cnt2++;
if (n<3) return 1;
if(f[n]!=-1) return f[n];//如f(n)曾计算过就不用再递归计算了,直接返回记录的值
f[n]=Fibonacci2(n-1)+Fibonacci2(n-2);
return f[n];
}
int main(){
memset(f,-1,sizeof(f));
int a;
cin >> a;
//cout << Fibonacci1(a)<<","<<cnt1<<endl; //这种写法,输出的cnt1总是0
cout << Fibonacci1(a)<<endl;
cout<<cnt1<<endl;
cout << Fibonacci2(a)<<endl;
cout<<cnt2;
return 0;
}
输入
20
输出
6765
13529
6765
37
factorial2就是记事本方法,把曾经计算过的结果保存起来,避免再重复计算。
因为factorial1, 每次我们在求f(n -1) 和 f(n -2)时 ,是分别进行递归的, 因此很多东西实际上是重复计算了的,函数调用了13529次。采用记事本方法,则函数只调用了37次。
递归例3:尾递归(编译器优化函数调用)
( 阶乘耗时太短,不适合演示)
#include <bits/stdc++.h>
using namespace std;
long long f[100];
long long cnt1=0,cnt2=0,cnt3=0;
long long Fibonacci1( int n){
cnt1++;
if (n<3) return 1;
return Fibonacci1(n-1)+Fibonacci1(n-2);
}
long long Fibonacci2( int n){ //记事本方法
cnt2++;
if (n<3) return 1;
if(f[n]!=-1) return f[n];//如f(n)曾计算过就不用再递归计算了,直接返回记录的值
f[n]=Fibonacci2(n-1)+Fibonacci2(n-2);
return f[n];
}
long long Fibonacci3( int n,int a,int b){
cnt3++;
if (n==1) return a; //当尾递归执行到最后时,直接返回最终结果。
return Fibonacci3(n-1,b,a+b);//尾递归,类似于 迭代的写法
}
int main(){
memset(f,-1,sizeof(f));
int a; a=40;
//cin >> a;
clock_t start_t,stop_t;
start_t = clock();
cout << Fibonacci1(a)<<endl;
stop_t = clock();
cout<<cnt1<<endl;
cout<<"函数执行时间="<<double(stop_t - start_t)<<"ms"<<endl<<endl;
start_t = clock();
cout << Fibonacci2(a)<<endl;
stop_t = clock();
cout<<cnt2<<endl;
cout<<"函数执行时间="<<double(stop_t - start_t)<<"ms"<<endl<<endl;
start_t = clock();
cout << Fibonacci3(a,1,1)<<endl;
stop_t = clock();
cout<<cnt3<<endl;
cout<<"函数执行时间="<<double(stop_t - start_t)<<"ms"<<endl<<endl;
return 0;
}
结果为:
102334155
204668309
函数执行时间=591ms
102334155
77
函数执行时间=0ms
102334155
40
函数执行时间=0ms
对于尾递归来说,永远不用保存外层函数信息,实际只存在一个调用记录,所以永远不会发生"栈溢出"错误。
例如:递归函数需要用到一个中间变量total(实际上可能没在代码中体现),那就把这个中间变量写到函数的参数(即把 运算符公式写到函数参数中),这样写的缺点就是不太直观。
尾递归的特点(缺一不可)
1,对函数自身的调用在尾部,递归调用是当前函数执行的最后一个操作。
2,尾递归会把当前的运算结果(或路径)放在参数里传给下一次递归函数,使得最后一个语句return 最终结果。
如本例mian调用方法:Fibonacci3(a,1,1)
递归调用方法:Fibonacci3(n-1,b,a+b);//尾递归,类似于 迭代的写法,a+b是核心计算公式
最终结果:if (n==1) return a; //当尾递归执行到最后时,直接返回最终结果。
阶乘的写法
long long factorial3(int n,long long result){ //尾递归
cnt3++;
if(n==1) return result;
return factorial3(n-1,n*result);
}
cout<<factorial3(n,1)<<endl;
用加法写的递归。
#include <bits/stdc++.h>
#include <sys/time.h>
using namespace std;
long long cnt1=0,cnt2=0;
long long mysum1( long long n){
cnt1++;
if (n==1) return 1;
return n+mysum1(n-1);
}
long long mysum2( long long n,long long a){
cnt2++;
if (n==1) return a+n;
return mysum2(n-1,a+n);
}
int main(){
long long n; n=50000;
//cin >> a;
clock_t start_t,stop_t;
start_t = clock();
cout << mysum1(n)<<endl;
stop_t = clock();
cout<<cnt1<<endl;
cout<<"函数执行时间="<<double(stop_t - start_t)<<"ms"<<endl<<endl;//看来不够精确
cnt1=0;
struct timeval start,end;
gettimeofday(&start, NULL );
cout << mysum1(n)<<endl;
gettimeofday(&end, NULL );
cout<<cnt1<<endl;
double timeuse = ( end.tv_sec - start.tv_sec ) + (end.tv_usec - start.tv_usec);
cout<<"函数执行时间="<<timeuse<<"微秒"<<endl<<endl;
gettimeofday(&start, NULL );
cout << mysum2(n,0)<<endl; //尾递归
gettimeofday(&end, NULL );
cout<<cnt2<<endl;
timeuse = ( end.tv_sec - start.tv_sec ) + (end.tv_usec - start.tv_usec);
cout<<"函数执行时间="<<timeuse<<"微秒"<<endl<<endl;
return 0;
}
sum(5)
5+ sum(4)
5+ (4+ sum(3))
5+ (4+ (3+ sum(2)))
5+ (4+ (3+ (2+ sum(1))))
5+ (4+ (3+ (2+ 1)))
= 15
可见这种方法空间复杂度为O(n),无论是时间上还是空间上的消耗都很大,极易出现爆栈的情况,
使用尾递归
sum(5,0)
sum(4,5)
sum(3,9)
sum(2,12)
sum(1,14)
sum(0,15)= 15
由于尾递归只需要保留最后一个函数堆栈,不需要保存很多中间函数的堆栈,所以空间复杂度为O(1),因此大大提高了执行效率
递归例4:递归原理观察:后入先出与翻转字符串
给出一个值4267,我们需要依次产生字符‘4’,‘2’,‘6’,和‘7’。
首先我们会想到用4267取余,然后除以10再区域,如此循环。但这样输出的顺序不会是7,6,2,4吗?于是我们就利用递归的堆栈结构的特性:
先进后出(即后进先出,4作为函数参数,是最后产生的,但反而最先执行cout)
#include <bits/stdc++.h>
using namespace std;
void pd( int n){
if(n/10!=0) pd(n/10);
cout<<n%10; //一直到n/10==0,此时n=4时才第一次执行到这一行,然后回到栈上一层的n=42,%10=2,然后上一层的n=426。。。
}
int main(){
int a;
cin >> a;
pd(a) ;
return 0;
}
4276
4276
#include <bits/stdc++.h>
using namespace std;
void pd( int n){
cout<<n%10; //类似于循环的写法,先入先出
if(n/10!=0) pd(n/10);
}
int main(){
int a;
cin >> a;
pd(a) ;
return 0;
}
4276
6724
递归函数中,位于递归调用语句后的语句的执行顺序和各个被调用函数的顺序相反.
即位于递归函数入口前的语句,从外往里执行;位于递归函数入口后面的语句,由里往外执行。
翻转字符串的变化过程
#include <bits/stdc++.h>
using namespace std;
char* pd(char *str){
int len=strlen(str);
if(len>1){
char ctemp=str[0];
str[0]=str[len-1];
str[len-1]='\0';
cout<<"ctemp,len,str="<<ctemp<<","<<len<<","<<str<<endl;
pd(str+1);
cout<<"return"<<endl;
cout<<"ctemp,len,str="<<ctemp<<","<<len<<","<<str<<endl;
str[len-1]=ctemp; //恢复现场, ctemp从原始字符串开始向后移动并存储,递归返回后将ctemp从后补回
cout<<"ctemp,len,str="<<ctemp<<","<<len<<","<<str<<",从后补字符"<<ctemp<<endl;
}
return str;
}
int main(){
//char *str="abcdefg";这样会报waring但不会报err. [Warning] deprecated conversion from string constant to 'char*' [-Wwrite-strings]
//warning的原因是字符串常量存放在const内存区,而字符串指针变量定义却是指向char型,const内存区当然不会让你想改就改的。所以,如果你一定要写这块内存的话,那就是一个非常严重的内存错误。但如果只是查询字符,则不会出错。
char str[10]="12345678";
cout<<"最终结果"<<pd(str);
return 0;
}
ctemp,len,str=1,8,8234567
ctemp,len,str=2,6,73456
ctemp,len,str=3,4,645
ctemp,len,str=4,2,5
return
ctemp,len,str=4,2,5
ctemp,len,str=4,2,54,从后补字符4
return
ctemp,len,str=3,4,654
ctemp,len,str=3,4,6543,从后补字符3
return
ctemp,len,str=2,6,76543
ctemp,len,str=2,6,765432,从后补字符2
return
ctemp,len,str=1,8,8765432
ctemp,len,str=1,8,87654321,从后补字符1
最终结果87654321
递归例5:走楼梯
题目1:求方案总数(从0台阶向上走,或从n台阶向下走,记事本)
爬一个有n个台阶的楼梯,每次只能上1个或者2个台阶,那么到达顶端共有多少种不同的方法?
方法1:
#include<bits/stdc++.h>
using namespace std;
long long pd(int n){
if(n<=2) return n;
return pd(n-1)+pd(n-2);
}
int main(){
int n;cin>>n;
cout<<pd(n);
return 0;
}
方法2:
#include<bits/stdc++.h>
using namespace std;
int n;
long long pd(int m){
if(m==n) return 1;
else if(m>n) return 0; //有的网站测试点会让 if(m>=n) return 1;
return pd(m+1)+pd(m+2);
}
int main(){
cin>>n;
cout<<pd(0);
return 0;
}
方法3:记事本方法,减少了大部分递归调用次数。避免超时
#include<bits/stdc++.h>
using namespace std;
long long f[100];
long long pd(int n){
if(n==0) return 0;
if(n==1) return 1;
if(n==2) return 2;
if(!f[n-1]) f[n-1]=pd(n-1);
if(!f[n-2]) f[n-2]=pd(n-2);
return f[n-1]+ f[n-2];
}
int main(){
int n;cin>>n;
cout<<pd(n);
return 0;
}
题目2:列出各方案详细路径
爬一个有n个台阶的楼梯,每次只能上1个或者2个台阶,那么到达顶端有多种不同的方法,请打印出所有方案。
#include<bits/stdc++.h>
using namespace std;
int n;
int f[100],cnt;
int pd(int m,int re){
m+=re;
if(re>0) f[cnt++]=re;
if(m==n) {
for(int i=0;i<cnt;i++) cout<<f[i]<<" ";
cout<<endl;
return 1;
}
else if(m>n) return 0; //有的网站测试点会让 if(m>=n) return 1;
return pd(m,1)+pd(m,2);
}
int main(){
cin>>n;
pd(0,0);
return 0;
}
上面代码打0分,原因是cnt是对所有递归路径都生效的一个全局变量。
必须把2路递归理解成2叉树,路径、跨过台阶数 只能在递归调用的参数中分开传递
#include<bits/stdc++.h>
using namespace std;
int n;
int f[100];
int pd(int m,int re,int cnt){
m+=re;
if(re>0) f[cnt++]=re;
if(m==n) {
for(int i=0;i<cnt;i++) if(f[i]>0) cout<<f[i]<<" ";
cout<<endl;
return 1;
}
else if(m>n) return 0; //有的网站测试点会让 if(m>=n) return 1;
return pd(m,1,cnt+1)+pd(m,2,cnt+1);
}
int main(){
cin>>n;
pd(0,0,0);
return 0;
}