函数与递归

前面我们学习了很多C++标准函数,但是这些标准函数并不能满足所有需求。当我们需要某特定功能的函数时,这就需要我们学会自定义函数,根据需求定制想要的功能。

1、自定义函数

自定义函数语法如下

返回类型 函数名(参数列表)
{
	函数体
}

说明:
(1)函数名是标识符,一个程序中除了主函数名必须为main外,其余函数的名字按照标识符的取名规则命名。
(2)自定义函数符合“根据已知计算未知”这一机制,参数列表相当于已知,是自变量,函数名相当于未知,是因变量。
(3)参数列表可以是空的,即无参函数,也可以有多个参数,参数间用逗号隔开,不管有没有参数,函数名后的括号不能省略。参数列表中的每个参数,由参数类型说明和参数名组成。
(4)函数体是实现函数功能的语句,除了返回类型是void的函数,其他函数的函数体中至少有一条语句是“return 表达式;”用来返回函数的值。执行函数过程中碰到return语句,将在执行完return语句后直接退出函数,不去执行后面的语句。
(5)返回值的类型一般是int、double、char等类型,也可以是数组。有时函数不需要返回任何值,例如函数只是用cout、printf向屏幕输出一些内容或处理全局变量数组,这时只需定义函数返回值类型为void,并且无须使用return返回函数的值。

根据上述定义,我们知道C++函数形态有以下四类:
(1)返回类型 函数名(参数列表)
(2)返回类型 函数名()
(3)void 函数名(参数列表)
(4)void 函数名()
下面我们一起来看几个例子:

例1:给定两个非负整数n和m,编写函数计算组合数 C n m C_n^m Cnm

分析:根据已知n和m,计算未知 C n m C_n^m Cnm。设计以下函数
long long C(int n,int m)
其中函数的返回值为 C n m C_n^m Cnm,返回类型为long long,函数名为C,参数列表中有两个参数n、m,类型都是int。函数体是实现函数功能的语句,根据 C n m C_n^m Cnm= n ! m ! ∗ ( n − m ) ! \frac{n!}{m!*(n-m)!} m!(nm)!n!发现需要三次用到“计算一个数的阶乘”这个功能,因此可以把这个功能独立出来设计一个函数来实现:
long long f(int n)
该函数的返回值为n!,返回值类型为long long,函数名为f,需要一个参数n,类型为int。
综上,该函数的代码如下:

#include<bits/stdc++.h>
using namespace std;

long long f(int n){
	long long ans=1;
	for(int i=1;i<=n;i++)
		ans*=i;
	return ans;
}

long long C(int n,int m){
	return f(n)/(f(m)*f(n-m));
}

int main(){
	int n,m;
	cin>>n>>m;
	cout<<C(n,m)<<endl;
	return 0;
} 

提示:
(1)函数体中的语句可以是对另一个函数的调用。
(2)被调用的函数需写在调用的函数前面。因本程序中main()调用C()、C()调用f(),所以C()写在main()前面,f()写在C()前面。如果需要调用后面定义的函数,就要先声明该被调用的函数。声明方法:返回类型 函数名(参数列表);
故上述代码也可改写为如下:

#include<bits/stdc++.h>
using namespace std;
long long C(int n,int m);
long long f(int n);

int main(){
	int n,m;
	cin>>n>>m;
	cout<<C(n,m)<<endl;
	return 0;
} 

long long C(int n,int m){
	return f(n)/(f(m)*f(n-m));
}

long long f(int n){
	long long ans=1;
	for(int i=1;i<=n;i++)
		ans*=i;
	return ans;
}

(3)对于较大的n,m来说,上述程序可能会溢出,可用“高精度”算法解决。

例2:编写函数输出斐波那契数列(1、1、2、3、5、8、13……)的第n项。

分析:因为该程序直接在函数中输出结果,可以不需要返回值,所以该函数的返回类型为void,函数体部分只需计算出第n项并输出即可。

#include<bits/stdc++.h>
using namespace std;
void f(int n);

int main(){
	int n;
	cin>>n;
	f(n);
	return 0;
} 

void f(int n){
	if(n<=2)
		cout<<1;
	else{
		int a=1,b=1,c=1;
		for(int i=3;i<=n;i++){
			c=a+b;
			a=b;
			b=c;
		}
		cout<<c;	
	}
}

2、函数调用与参数传递

2.1调用方法

对于没有返回值的函数,调用时直接单独一行写“函数名(参数);”即可。如例2中"f(n);"。

对于有返回值的函数,调用时必须以值的形式出现在表达式中。如例1中“return f(n)/(f(m)*f(n-m));”。

程序可以调用任何前面已经定义的函数,如果需要调用后面定义的函数,就要先声明该被调用的函数。声明方法:返回类型 函数名(参数列表);

2.2形式参数与实际参数

函数定义中的参数名称为形式参数,如例2中“void f(int n)”的n是形式参数。我们完全可以把n换成a,再把函数体中的n换成a,函数的功能完全一样。

实际参数是指实际调用函数时传递给函数形参的值。如例2主函数中使用f(n);调用函数时n的值就是实际参数。

2.3函数调用的执行过程

(1)计算实际参数的值。
(2)将实际参数传递给被调用函数的形式参数,程序执行跳到被调用的函数中。
(3)执行函数体,执行完后如果有返回值,则把返回值返回给调用该函数的地方继续执行。

2.4.1传值参数

前面的程序中都采用了传值参数,改变函数中形参的值并不会影响外部实参的值。
例如:

#include<bits/stdc++.h>
using namespace std;

void swap(int a,int b){
	int t=a;
	a=b;
	b=t;
}

int main(){
	int x,y;
	cin>>x>>y;
	swap(x,y);
	cout<<x<<' '<<y<<endl;
	return 0;
} 

执行上述程序输入1 2,结果仍未1 2。这是因为swap函数的参数传递是值传递,调用swap函数时传递给形参a b的是实参x y的副本,swap函数内a b的变化只是交换了实参x y的副本,而实参x y并没有被交换。

2.4.2引用参数

函数定义时在形参之前加“&”,则该参数就是引用参数,“&”是取地址操作符,引用参数传递时将实参的内存地址传递给形参,即形参与实参为同一内存地址,此时形参实参两者绑定,形参的变化即是实参的变化。
例如:

#include<bits/stdc++.h>
using namespace std;
void swap(int &a,int &b){
	int t=a;
	a=b;
	b=t;
}
int main(){
	int x,y;
	cin>>x>>y;
	swap(x,y);
	cout<<x<<' '<<y<<endl;
	return 0;
} 

此时输入1 2,结果为2 1。将第3行与第11行放到一起看实参传递给形参的过程相当于声明:a是x的引用(int &a=x;),b是y的引用(int &b=y;)。在子函数中,a是&x的符号地址,b是&y的符号地址,或者说a成为变量x地址(&x)的符号名的别名,b成为变量y地址(&y)的符号名的别名,子函数执行时,操作a和b就等同于操作x和y。

3、变量作用域

作用域表示变量在程序的多大范围内可见可使用。C++程序中的变量按作用域来分,有全局变量和局部变量。

3.1全局变量

定义在函数外部没有被花括号括起来的变量称为全局变量。全局变量的作用域从变量定义的位置开始到文件结束。
优点:
(1)全局变量使得函数间多了一种信息传递的方式。如果在一个程序中的多个函数都需要对同一个变量或数组进行处理,可以将这个变量定义成全局变量。
(2)全局变量在定义时如果没有赋初值,其默认值为0。因为通常将数组定义为全局变量,可省去赋初值的操作。
缺点:
(1)全局变量在程序执行的过程中会一直占用内存单元。
(2)过多地使用全局变量,会增加调试难度。因为多个函数都能改变全局变量的值,不易判断某个时刻全局变量的值。
(3)过多地使用全局变量,会降低程序的通用性。将一个函数移植到另一个程序中时需要将全局变量一起移植过去,同时还有可能出现重名问题。

3.2局部变量

定义在函数内部的变量成为局部变量。例如函数的实参、形参、for循环中定义的变量都称为局部变量。
说明:
(1)局部变量只在块内可见,在块外无法访问,具有块作用域。例如for(int i=1;i<=n;i++)中的i是在for循环中定义的,存在时间和作用域只限制在for循环语句中。
(2)不同函数的局部变量相互独立,不能访问其他函数的局部变量。
(3)局部变量的存储空间是临时分配的,当作用域执行完毕时,局部变量的空间就被释放,其中的值无法保留到下次使用。
(4)定义在内部作用域的名字会自动屏蔽在外部作用域相同的名字。当一个局部变量的作用域结束时,它对全局变量的屏蔽会被取消。

4、函数的应用

例1:最大公约数

输入两个数,输出它的最大公约数

#include<bits/stdc++.h>
using namespace std;

int gcd(int x,int y){
	for(int i=x;i>=1;i--)
		if(x%i==0 && y%i==0)
			return i;
}

int main(){
	int x,y;
	cin>>x>>y;
	cout<<gcd(x,y)<<endl;
	return 0;
} 

例2:阶乘(factorial)

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。
计算3!+7!+11!

#include<bits/stdc++.h>
using namespace std;

int fac(int x){
	int s=1;
	for(int i=1;i<=x;i++)
		s*=i;
	return s;
}

int main(){
	cout<<fac(3)+fac(7)+fac(11)<<endl;
	return 0;
} 

例3:孪生素数

输出1000以内所有的孪生素数对(差值为2)以及总对数。

#include<bits/stdc++.h>
using namespace std;

bool prime(int x){
	bool s=true;
	for(int i=2;i<x;i++)
		if(x%i==0)
			s=false;
	return s;
}

int main(){
	int s=0;
	for(int i=3;i<=997;i+=2)
		if(prime(i)&&prime(i+2))
			s+=1;
	cout<<s<<endl;
	return 0;
}

例4:数字统计

统计在给定范围[L,R]的所有整数中,数字2出现的次数。
比如在给定范围[2,22],数字2在2中出现了1次,在12中出现了1次,在20中出现了一次,在21中出现了1次,在22中出现了两次,所以一共出现了6次

#include<bits/stdc++.h>
using namespace std;

int tj(int x){
	int s=0;
	while(x){
		if(x%10==2)
			s+=1;
		x=x/10;
	}
	return s;
}

int main(){
	int l,r;
	cin>>l>>r;
	int s=0;
	for(int i=l;i<=r;i++)
		s+=tj(i);
	cout<<s<<endl;
	return 0;
}

例5:火柴数字

火柴数字如下图:
在这里插入图片描述
现用6根火柴摆数字,请列出所有能摆出的自然数,要求每个数火柴全用上,不多不少。

【问题分析】
0~9每根所用的火柴棒数量:

数字0123456789
火柴棒6255456376

可以看出:1最少,用了2根,8最多,用了7根。不难分析出数字范围是[0,111]之间。
思考7根火柴棒的范围?[8,711] 18根火柴?
知道了数据范围,且每个数需使用6根火柴的条件明确,此时我们可以考虑使用穷举法:将[0,111]的每个数的所使用的总火柴数统计,若为6则输出。
算法:通过循环列出 [0,111] 的每个数,再写一个内层循环判定每个数所使用的的火柴个数(与上面的数字统计类似),整体上是一个双层循环。外层从0取到111,内层把每个数的每一位所使用火柴数的和算出来,0~9十个数字的火柴数可以用列表来表示。参考程序如下:

#include<bits/stdc++.h>
using namespace std;
int main(){
	int f[]={6,2,5,5,4,5,6,3,7,6};	//0~9分别需要多少根火柴棒
	cout<<0<<endl;			//对0进行特殊处理,请思考为什么?
	for(int i=1;i<112;i++){	//穷举范围
	    int s=0;			//统计总数目
	    int ti=i;			//因i会在内层循环时发生改变,在此对i进行保存以便后续使用
	    while(ti){			//计算ti需要多少根火柴棒,ti非0才会执行循环,所以0不在计算范围
	    	s+=f[ti%10];	//个位所用的数量
	    	ti=ti/10;		//整除10,除去个位数
	    }			 		
	    if(s==6)
	        cout<<i<<endl;
	}
	return 0;
}

代码复用可以提高程序的效率,函数C++中代码复用的常见方法,在此可以考虑将统计数字i所使用的的火柴棒数量这一段代码封装为函数进行复用,既提高了效率也增强了代码的可读性。参考程序如下:

#include<bits/stdc++.h>
using namespace std;

int tj(int x){
	int f[]={6,2,5,5,4,5,6,3,7,6};	//0~9分别需要多少根火柴棒
	int s=0;			//统计总数目
	int ti=x;			//因x会在内层循环时发生改变,在此对i进行保存以便后续使用
    while(ti){			//计算ti需要多少根火柴棒,ti非0才会执行循环,所以0不在计算范围
    	s+=f[ti%10];	//个位所用的数量
    	ti=ti/10;		//整除10,除去个位数
    }
    return s;
}
int main(){
	cout<<0<<endl;			
	for(int i=1;i<112;i++)	//穷举范围
	    if(tj(i)==6)
	        cout<<i<<endl;
	return 0;
}

5、递归函数

递归是计算科学领域中一种重要的计算思维模式。它既是一种抽象表达的手段,也是一种问题求解的重要方法。直接或间接地调用自身的方法称为递归,递归分为递推和回归。指一种通过重复将问题分解为同类的子问题,从而解决问题的方法。
在数学与计算机领域中,递归函数是指用函数自身来定义该函数的方法。如著名的斐波那契数列"1 1 2 3 5 8 13 …",可以递归定义为

F ( n ) = { 1                                        ( n = 1 或 n = 2 ) F ( n − 1 ) + F ( n − 2 )     ( n > 2 ) F(n)=\begin{cases}1\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space(n=1或n=2)\\ F(n-1)+F(n-2)\space\space\space\space(n>2) \end{cases} F(n)={1                                       n=1n=2F(n1)+F(n2)    n>2

递推关系是递归的重要组成,例如上式中的 F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n)=F(n-1)+F(n-2) F(n)=F(n1)+F(n2);而边界情况是递归的另一要素,例如上式中当 n 为 1 或 2 时的 F ( n ) = 1 F(n)=1 F(n)=1,它保证递归能在有限次的计算后得出结果,而不会产生无限循环的情况。

【样例研习】
以下为用递归算法求斐波那契数列第n项的C++函数

int fibo(int n){
    if(n==1 || n==2)
        return 1;
    else
        return fibo(n-1)+fibo(n-2);
}

本题的递推公式为 f i b o ( n ) = f i b o ( n − 1 ) + f i b o ( n − 2 ) fibo(n)=fibo(n-1)+fibo(n-2) fibo(n)=fibo(n1)+fibo(n2),边界条件为 f i b o ( 1   o r   2 ) = 1 fibo(1 \space or \space 2)=1 fibo(1 or 2)=1
求解过程如下:

f i b o ( 5 ) = f i b o ( 4 ) + f i b o ( 3 ) = ( f i b o ( 3 ) + f i b o ( 2 ) ) + ( f i b o ( 2 ) + f i b o ( 1 ) ) = ( ( f i b o ( 2 ) + f i b o ( 1 ) ) + 1 ) + ( 1 + 1 ) = ( 1 + 1 ) + 1 ) + ( 1 + 1 ) = 5 fibo(5)\\ =fibo(4)+fibo(3)\\ =(fibo(3)+fibo(2))+(fibo(2)+fibo(1))\\ =((fibo(2)+fibo(1))+1)+(1+1)\\ =(1+1)+1)+(1+1)\\ =5 fibo(5)=fibo(4)+fibo(3)=(fibo(3)+fibo(2))+(fibo(2)+fibo(1))=((fibo(2)+fibo(1))+1)+(1+1)=(1+1)+1)+(1+1)=5

递归算法需确定的两个条件:
1、递推关系
2、边界条件(即递归退出的条件)

常用的递归代码框架:

函数类型 函数名(形式参数):
	if(边界条件)
		语句组
	else
		递推公式

递归与迭代:
(1)递归与迭代算法都需要重复执行某些代码
(2)递归是重复调用函数自身,遇到满足终止条件时逐层返回;迭代是重复反馈过程,其目的是逼近所需目标或结果,通常使用计数器结束循环。

6、递归的应用

1、斐波那契数列:
1 1 2 3 5 8 13 21 34 55 89 ...
已知前两项为1,之后每一项等于前两项之和。
现输入n,请输出兔子数列的第n项。

2、用递归法求4!+5!+6!+7!的值。
F ( n ) = { 1                          ( n = 0 ) n ∗ F ( n − 1 )     ( n > 0 ) F(n)=\begin{cases}1\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space(n=0)\\ n*F(n-1)\space\space\space\space(n>0) \end{cases} F(n)={1                         n=0nF(n1)    n>0
以下为用递归算法求自然数n的阶乘的Python程序

long long fac(int n){
    if(n==0)
        return 1;
    else
        return n*fac(n-1);
}

本题的递推公式为 f a c ( n ) = f a c ( n − 1 ) ∗ n fac(n)=fac(n-1)*n fac(n)=fac(n1)n,边界条件为 f a c ( 0 ) = 1 fac(0)=1 fac(0)=1
求解过程如下:

f a c ( 4 ) = 4 ∗ f a c ( 3 ) = 4 ∗ ( 3 ∗ f a c ( 2 ) ) = 4 ∗ ( 3 ∗ ( 2 ∗ f a c ( 1 ) ) ) = 4 ∗ ( 3 ∗ ( 2 ∗ ( 1 ∗ f a c ( 0 ) ) ) ) = 4 ∗ ( 3 ∗ ( 2 ∗ ( 1 ∗ 1 ) ) ) fac(4)\\ =4*fac(3)\\ =4*(3*fac(2))\\ =4*(3*(2*fac(1)))\\ =4*(3*(2*(1*fac(0))))\\ =4*(3*(2*(1*1))) fac(4)=4fac(3)=4(3fac(2))=4(3(2fac(1)))=4(3(2(1fac(0))))=4(3(2(11)))

3、用递归法求1+2+3+…+100的值。

4、输入两个数,求其最大公约数。
根据上文中所讲的辗转相除法可得公式如下:
g c d ( a , b ) = { b                       ( a % b = 0 ) g c d ( b , a % b )     ( a % b ≠ 0 ) gcd(a,b)= \begin{cases}b\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space(a\%b=0)\\ gcd(b,a\%b)\space\space\space\space(a\%b\neq 0) \end{cases} gcd(a,b)={b                      a%b=0gcd(b,a%b)    a%b=0

5、花果山上有一洞,小猴每次采取跳1阶或者跳3阶的办法从山下跳跃上台阶进洞,编程输入台阶数,输出有多少种不同的跳法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

以太以北

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值