函数与递归
1.函数的介绍
(1)什么是函数
函数是【完成特定任务的独立代码单元】
语法规则定义了函数的结构和使用方法。一些函数执行某些动作,如printf()把数据打印在屏幕上;一些函数找出一个值供程序使用,如strlen()把指定字符串长度返回给程序。一般而言,函数可用同时具备以上两种功能。
(2)为什么使用函数
为什么使用函数?
首先,函数可用省去编写重复代码的苦差。如果程序多次完成某项任务,那么只需要编写一个函数,在需要时使用该函数,或者在不同的程序中使用该函数,就像每次调用printf()函数一样,写一次函数代码就能方便多次的使用。
其次,即使程序只完成一次某项任务,也值得使用函数。因为函数让代码更加模块化,增强了代码的可读性,同时也方便了后期的修改完善。
(3)简单函数的实现
上面说了这么多,让我们来简单实现一个加法函数add
int add(int x, int y)//x,y形式参数(形参),只有函数被调用的时候才去实例化(分配内存空间),调用完成自动销毁,平时不占内存空间
{
int z = x + y;
return z;
}
int main()
{
int a, b;
scanf("%d%d", &a, &b);
int sum = add(a, b);//a,b实际参数(实参),无论何种形式(常量.变量.表达式.函数等)必须有确定的值
printf("sum=%d", sum);
return 0;
我们在主函数外设置了一个add函数,我们在主函数中调用add函数,传递参数到函数中,函数就会将两个数相加,并返回相加后的值。
我们发现,上面函数中有实参和形参两种参数。
实参,即实际参数,是我们在主函数中调用函数是传递的参数。
形参,即形式参数,是我们在自定义函数的过程中定义的参数。
函数的运行过程中,将实参的值传递到形参。
2.详细分析函数
(1)自定义函数
函数可分为库函数和自定义函数。
库函数是函数库中自带的函数,库函数使用方便简单,只要调用对应的头文件就能使用相应的库函数。在这里我们详细介绍自定义函数。
自定义函数的结构如下
ret_type fun_name (paral, *)
返回类型 函数名 函数参数
{ //---------函数体,交代函数如何运作
statement;//语句项
}
以下函数为例子。
int add(int x, int y)//返回类型为int 函数名为add 两个参数为int型的x和y
{
int z = x + y;
return z;//返回z
}
让我们来练习一个函数,实现求两个数的最大值。实现如下:
int M1(int x, int y)
{
int c =(x > y ? x : y);
return c;
}
int M2(int x, int y)
{
if (x > y)
return x;
else
return y;
}
int main()
{
int a, b;
scanf("%d%d", &a, &b);
int max1 = M1(a, b);
int max2 = M2(100, 12);
printf("max = %d and %d", max1,max2);
return 0;
}
如上,我们分别设计了M1 M2两个函数,均能达成求最大值的操作。
参数方面: 我们规定两函数均接收int类型的参数(形参为int型),那我们在传递实参时只能传递int型。
若传递了其他类型的实参,编译器会将其他类型自动强制转换成形参规定的int型传递到形参中,并警告或直接报错。返回值方面,两函数返回类型均为int型,返回值为两参数中的最大值
(2)函数的参数
上面我们已经区分了实参与形参,当我们熟练掌握传参后,我们来拓展一下参数相关的知识。
注意:
【传值调用】
当实参传给形参时,形参其实是实参的一份临时拷贝,对形参修改是不会影响实参的值。
【传址调用】
但是,如果我们传递的参数为指针,形参和实参指向同一个对象。当我们修改形参指针指向的对象时,由于实参也指向同一对象,该对象的值也随之被修改。
我们在以下代码中详细阐述传值调用和传址调用的区别:
void swap1(int x, int y)//此函数不能实现交换
{
int z = x;
x = y;
y = z;
}
这里x y虽接受了a b的值,但是和主函数的a b并无直接的联系,改变x y不会影响到a b。
整形a和b在函数swap内为按值传递,按值传递时,函数不会访问当前调用的实参。
函数处理的值是它本地的拷贝,这些拷贝被存储在运行栈中,因此改变这些值不会影响实参的值。
一旦函数结束了,函数的活动记录将从栈中弹出,这些局部值也就消失了。
void swap2(int* px, int* py)
{
int z = *px;
*px = *py;
*py = z;
}
在swap2函数中,由于传递的是两个变量的内存地址(指针),使得我们可以直接操作对应的值。
实际上这里还是存在按值传递的问题(其实并没对x y本身做了改变,x y仍指向原来的地址,只是地址中的值发生了变化)只是由原先的整形传递变成了指针传递。
我们可以修改指针指向的内存却依然无法修改指针本身。
int main()
{
int a = 30;
int b = 60;
int* pa = &a;//pa是a对应的指针变量 ; *pa是解引用操作(通过pa地址找到pa对应空间中存放的内容)
int* pb = &b;
printf("交换前a=%d,b=%d\n", a, b);
swap1(a, b);//不能实现交换
swap2(pa, pb);//可以实现
swap2(&a, &b);
printf("交换后a=%d,b=%d\n", a, b);
return 0;
}
根据以上推出结论:
1.传值调用
函数的实参和形参分别占有不同的内存块,对形参的修改不会影响实参
2.传址调用
把函数外部创建变量的地址传递给函数参数
可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
之前求a b的最大值,只是找最值,不改变a b,所以用传值调用。
现在交换a b的值,要改变a b所以用传址调用。
(3)函数的返回值
返回类型
返回类型对应的返回值,就是子程序或者函数执行结束后,将函数实现的结果返回给调用者。
return语句
return 语句的作用是终止一个函数的执行,结束当前代码块中return后的语句,从"当前位置"退出,为该函数返回一个指定的expression 值。
“当前位置退出” 可指从递归函数某次递归回到上一次递归。
return 的作用 :
(1) return 从"当前的方法"中退出, 返回到该调用的方法的语句处, 继续执行。
(2) return 返回一个值给调用该方法的语句,返回值的数据类型必须与方法的声明中的返回值的类型一致,可以使用强制类型转换来是数据类型一致
(3)return 当方法说明中用void声明返回类型为空时,应使用这种格式,不返回任何值。
链式访问
把一个函数的返回值作为另外一个函数的参数
#include<string.h>
int main()
{
int arr="123";
printf("%d", strlen("123"));
return 0;
}
(4)函数的声明与定义
函数声明,即告诉编译器函数叫什么,参数是什么,返回类型是什么,但是函数具体是否存在无关紧要
// 函数声明 -- 程序运行从上到下,要是函数定义在函数调用下面,需要先在上面声明
int add(int, int);
int add(int x, int y); // 这样的形式也可以
int main()
{
int a, b;
scanf("%d%d", &a, &b);
//函数调用
int sum = add(a, b);
printf("sum=%d", sum);
return 0;
}
// 函数定义
int add(int x, int y)
{
int z = x + y;
return z;
}
工作中实际使用,会把函数声明放到其他.h文件中,把函数定义放在.c文件中。
此时需要以头文件的形式声明函数,如 #include"add.h", 声明库函数用< >,声明自定义函数用 " "
(5)函数的简单题目练习
一.判断一个数是否为素数的函数
void jud(int x)
{
int i;
if (x == 1)
printf("1是素数");
else
{
for (i = 2; i < x; i++)//优化:i<=sqrt(x);更号,对应#include<math.h>
{
if (x % i == 0)
{
printf("%d不是素数\n", x);
return 0;//遇到return 0;直接退出函数
}
}
printf("%d是素数\n", x);
}
}
int main()
{
int a;
scanf("%d", &a);
jud(a);
return 0;
}
二.判断一年是否为闰年函数
int run(int y)
{
if ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)
return 1;
else
return 0;
}
int main()
{
int year;
for (year = 1000; year <= 2000; year++)
{
if (1 == run(year))
printf("%d ",year);
}
return 0;
}
三.二分查找有序数列某数字k的下标的函数
int u(int arr1[], int x ,int right)//实参形参可取同样的名字
{
int left = 0;
//int right要在函数外部计算 ; 不可在函数内部求sizeof数组,因为数组传参得到时候,形参int arr1[]的实质是数组首元素的地址,而不是整个数组
while (left <= right)
{
int mid = (left + right) / 2;
if (x < arr1[mid]) right = mid - 1;
else if (x > arr1[mid]) left = mid + 1;
else if (x = arr1[mid]) return mid;
}
return -1;
}
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int right = sizeof(arr) / sizeof(arr[0]) - 1;
int k;
scanf("%d", &k );
传递数组首元素地址
int m = u(arr, k, right);
if (-1 == m)
printf("找不到下标\n");
else
printf("下标为%d",m);
return 0;
}
四.函数每调用一次,num就加一
若想让形参修改影响到实参,传址调用
void add(int* pn)
{
/**pn += 1;*/
(* pn)++;//不可写*p++,因为++对P优先度比*高,得不出想要结果
}
int main()
{
int num = 0;
while (num < 10)
{
add(&num);
}
printf("%d",num);
return 0;
}
3.递归函数
(1)递归概念及要点
递归:程序调用自身的编程技巧。
递归通常把大规模复杂为题层层转化为同类型规模较小的为题来解决。
思想是:把大事化小。
递归的三大要素:
【第一要素】:明确你这个函数想要干什么。
// 对于递归,很重要的一个事就是,这个函数的功能是什么,要完成什么样的一件事,而这个,是完全由自己来定义的。
// 我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。【第二要素】:寻找递归结束条件
// 所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。
// 也就是说,我们需要找出当参数是什么时,递归结束,之后直接把结果返回。
// 请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。【第三要素】:找出函数的等价关系式(重要)
// 第三要素就是,要不断缩小参数的范围,缩小之后,可以通过一些辅助的变量或者操作,使原函数的结果不变。
(2)简单的递归
下面是一段主函数递归的例子
int main()
{
printf("hhh\n");
main();
return 0;
}
上面函数会先死循环,栈区空间被沾满,然后提示 Stack overflow 栈溢出。
注意:内存会分区,栈区(局部变量.函数形参).堆区(动态开辟的内存).静态区(全局变量.static修饰的变量)
下面我们用实例来理解递归
题目:把1234每个数字拆开逐次打印为1 2 3 4
1234->(123) 4->(12) 3 4->(1) 2 3 4
void pri(int i)
{
if (i >= 10)
{
pri(i / 10);
}
printf("%d ",i%10);
}
int main()
{
int a = 1234;
pri(a);
return 0;
}
1.首先,明确递归要做什么。
我们要把 1234 拆开打印,即1234->(123) 4->(12) 3 4->(1) 2 3 4。
每次递归,打印参数最小位数字(数字%10)。
那么如果我们想要下次递归打印前一位,我们传递的参数数字就要去掉最小位(数字/10)。
2.其次我们要知道递归停止条件
参数若为个位数,停止递归,打印个位。
由上例得出: 递归的两个必要条件
1.存在限制条件,当满足这个限制条件时,递归便不再继续。
2.每次递归调用后需越来越接近这个限制条件
4.函数相关实践与题目解析
(1) 计算1 + 2 + 3 + … + 100
int f(int i)//计算数列和的函数
{
if (i > 0)
{
return f(i - 1) + i;// 这里的i是变量,代表‘本次’函数运行中的i,比如递归两次i从100变成98,那这里的i就是98
}
else
{
return 0;
}
}
int main()
{
int i,a;
scanf("%d", &i);
a = f(i);
printf("%d", a);
return 0;
}
(2)递归函数n的阶乘
//循环函数
int f1(int x)
{
int m = 1;
while (x >= 1)
{
m = m * x;
x--;
}
return m;
}
//递归函数
int f2(int x)
{
if (x > 1)
return (x * f2(x - 1));
else
return 1;
}
函数要做什么?
//函数为阶乘,即 n*(n-1)*...*3*2*1
//f(n)为n的阶乘,等于n*(n-1的阶乘),即n*f(n-1)
//那么 f2(x)要返回x*f(x-1)
结束条件是什么?
//x=1结束递归
int main()
{
int n;
scanf("%d", &n);
int r = f2(n);
printf("%d", r);
return 0;
}
(3)不用strlen计算字符串长度(不能创建临时变量)
//1.循环法函数(但是创建了临时变量n)
int len1(char* arr)//这里是数组的首元素地址的形式
{
int n = 0;//字符串长度n
while (*arr != '\0')// '\0'是字符串最后一个结束字符
{
n++;
arr++; //arr++意思是在地址上加了一个sizeof(char),然后就到下一个元素地址了
}
return n;
}
//2.递归函数
int len2(char* arr)
{
if (*arr != '\0')
{
return (1 + len2(arr + 1));
}
else
return 0;
}
//把大事化小
//len2("hello");
//1+len2("ello");
//1+1+len2("llo");
//1+1+1+len2("lo");
//1+1+1+1+len2("o");
//1+1+1+1+1+len2("");
//1+1+1+1+1+0=5
int main()
{
char arr[] = "hello";
int l = len2(arr);
printf("%d", l);
return 0;
}
(4) 求第n个斐波那契数列中的数值
斐波那契数:(1,1,2,3,5,8,13,21,34,55…)
1.递归
int f1(int n)
{
int x = 0;
if (n > 2)
{
x = f1(n - 1) + f1(n - 2);
}
else
{
return 1;
}
return x;
}
函数是什么?
//函数是第n项斐波那契数
//所以f(n)返回f(n-1)+f(n-2)
结束条件是是什么?
//前两项斐波那契数为1
//所以当n=1或2,返回1
2.循环
int f2(int n)
{
int a = 0; int b = 1; int c = 1;
int i = 1;
while (i <= n)
{
a = b;
b = c;
c = a + b;
i++;
}//第一次运行结束后,a,b,c 分别对应 1,1,2。之后i每加1,a对应的次数也加1
return a;
}
int main()
{
int n;
scanf("%d", &n);
int x = f2(n);
printf("%d", x);
return 0;
}
(5) 青蛙跳台阶
一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶.问要跳上第 n 级台阶有多少种跳法?
分析:
//4 3 2 1 0
//当N = 1时,当然只有1种跳法;
//当N = 2时,青蛙可以跳2次1层和跳1次2层;
//当N =3时,当有3层台阶时,青蛙可以选择先跳1层,剩下2层台阶,所以此时就是有2层台阶时的跳法,有2种;当青蛙选择第一次跳2层台阶时,剩下1层台阶,此时有1层台阶时的跳法,所以3层台阶时的方法是:2层台阶的方法+ 1层台阶的方法。
//当N = 4时,具体跳法为: 1、先跳1层 若先跳1层,则剩下3层,接下来就是3层台阶的跳法。 2、先跳2层 若先跳2层,则剩下2层,接下俩就是2层台阶的跳法,所以4层台阶的方法为:3层台阶的方法 + 2层台阶的方法。
//青蛙跳台阶
//1.函数法
int f1(int n) //f(n)代表跳n个台阶的方法种类
{
if (n > 2)
{
return f1(n - 1) + f1(n - 2);
}
else if (n == 2)
{
return 2;
}
else if (n == 1)
{
return 1;
}
}
//2.循环法
int f2(int n)
{
int a;
int b = 1;
int c = 2;
do
{
n--;
a = b;// 1 1 2 3 5 8 13 21 34 55
b = c;
c = a + b;
} while (n != 0);
return a;
}
int main()
{
int n;
scanf("%d", &n);
int x = f1(n);
printf("%d", x);
return 0;
}
(6) 汉诺塔问题(求n个盘子从A移到C,求最少移动次数)
有三根杆子A,B,C。 A杆上有N个(N > 1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆:
每次只能移动一个圆盘; 大盘不能叠在小盘上面。 提示:可将圆盘临时置于B杆,也可将从A杆移出的圆盘重新移回A杆,但都必须遵循上述两条规则。
运算移动的次数
方法:
第一步,把n-1个盘 A->B,把1个盘 A->C
第二步,把n-2个盘 B->A,把1个盘 B->C…
第N步 , 把1个盘移到C假设搬一个盘子(1),就是一次
假设搬两个盘子(2),是先把(1)搬到B柱,把最大盘A->C,然后把(1)B->C
假设搬三个盘子,先把(2)搬到B柱,把把最大盘A->C, 然后把(2)B->C
所以假设共n个盘子,搬运次数为f(n)=2f(n-1)+1当n = 1时,f(1) = 1
当n = 2时,f(2) = 2f(1) + 1
当n = 3时,f(3) = 2f(2) + 1
int f(int n)//函数为n个盘子的搬运次数
{
if (n == 1)
{
return 1;
}
else
{
return 2*f(n - 1) + 1;
}
}
int main()
{
int n;
int x;
scanf("%d", & n);//汉诺塔的层数
x = f(n);
printf("最少为%d次\n",x);
return 0;
}
(7) 有5个人坐在一起,每个比前一个大两岁,最后 问第1个人,他说是10岁。请问第12个人多大?
int year(int n)//年龄函数
{
if (n == 1)
{
return 10;
}
else
{
return year(n - 1) + 2;
}
}
int main()
{
int a;
int n = 5;
a=year(n);
printf("%d", a);
return 0;
}