1、函数可以嵌套调用
2、函数不可以嵌套定义
3、链式访问:指把一个函数的返回值作为另外一个函数的参数。
4、函数的声明和定义
函数声明:
1、告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但是具体是不是存在,无关紧要。
2、函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3、函数的声明一般要放在头文件中的。
函数定义:
函数的定义是指函数的具体实现,交代函数的功能实现
函数递归:
程序调用自身的编程技巧称为递归。递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小
递归的两个必要条件:
1、存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2、每次递归调用之后越来越接近这个限制条件。
例如:输入1234,输出1 2 3 4
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void print(unsigned int n)
{
if (n > 9)
{
print(n/10);
}
printf("%d “, n % 10);
}
int main()
{
unsigned int num = 0;
scanf(”%u", &num);
print(num);
return 0;
}
如何理解这段代码?
在主函数里,我们先把输入的数字放在print()函数里,即print(1234).
在print(1234)里,因为满足if语句,所以先执行print(n/10),即print(123).后面的printf("%d",1234%10)则先不执行。
满足如下图
print(1234)
print(123) 4
print(12) 3 4
print(1) 2 3 4
结果便是打印1 2 3 4
因为print(1)中的1不满足if语句,所以停止了递归,只需要执行打印语句即可
那么便会从print(1)开始,往前面执行打印语句,便会成我们所看到的结果
递归的栈溢出(Stack overflow)
内存里有一个区域是栈区,还有一个区域是堆区,还有一个区域是静态区
栈区里面放的是局部变量,函数形参。(里面放的都是临时的空间,临时的变量)
堆区是用来动态内存分配的(malloc/ free / calloc / realloc)
静态区是用来放全局变量以及静态变量的.
每一个函数的调用,都要在栈区上申请一块空间(栈帧空间)
函数里的空间,又会给变量划分空间。
如果是在一个函数里面调用另外一个函数,那么将会在栈区上额外申请一块新的空间。
当调用次数足够多的时候,那么栈空间最终会不够。从而导致栈溢出。
即递归层次太深,最终便会栈溢出。
写递归代码的时候:
1、不能死递归,得有跳出条件,每次递归要逼近跳出条件。
2、递归层次不能太深
模拟实现一个strlen函数
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int my_strlen(char* a)
{
int num = 0;
while (a != ‘\0’)
{
num++;
a++;
}
return num;
}
int main()
{
char arr[] = { “hello” };
printf("%d", my_strlen(arr));
return 0;
}
答案的结果便是5 ,因为字符串后面的’\0’是结束标志,并不会算进字符串长度里。
如果是sizeof来测量大小时,’\0’是会被算进去的。
注意:函数里传递的char a是数组的首地址,而不是整个数组的地址。
编写函数不允许创建临时变量,求字符串的长度。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int my_strlen(char* a)
{
if (*a != ‘\0’)
{
return 1 + my_strlen(a + 1);
}
else
{
return 0;
}
}
int main()
{
char arr[] = { “hello” };
printf("%d", my_strlen(arr));
return 0;
}
使用的依然是递归的思想
这个代码实现过程是如下:
my_strlen(“hello”);
1+my_strlen(“ello”);
1+1+my_strlen(“llo”);
1+1+1+my_strlen(“lo”);
1+1+1+1+my_strlen(“o”);
1+1+1+1+1+my_strlen("\0");
1+1+1+1+1+0
结果是5
里面的指针所接收的地址只是数组中的一个地址,每当你加上1,地址便会往后挪一位
因为数组里的地址是连在一起的,之间只相差1字节,所以(地址+1)便是往后移一位。
求n的阶乘(不考虑溢出)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int n = 0, ret = 1;
scanf("%d", &n);
for (int i = 1;i <= n;i++)
{
ret *= i;
}
printf("%d", ret);
return 0;
}
循环是一种迭代的方式
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int fac(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return n * fac(n - 1);
}
}
int main()
{
int n = 0, ret = 1;
scanf("%d", &n);
printf("%d", fac(n));
return 0;
}
有一些功能:可以使用迭代来实现,也可以使用递归
求第n个斐波那契数
先了解斐波那契数列是怎样的数列:
1 1 2 3 5 8 13 21 34 55 .。。。
我们可以通过观察得知,当n>2时,第n个数等于第n-1个数和第n-2个数的和
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int fib(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return fib(n - 1) + fib(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("%d", fib(n));
return 0;
}
但是这种写法效率太低-重复大量的计算,例如当我们输入50时,会发现结果迟迟不打印。
递归可以求解,但是效率太低。
我们也可以使用循环来解决问题
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int fib(int n)
{
int sum = 1, a = 1, b = 1;
while (n > 2)
{
sum = a + b;
a = b;
b = sum;
n–;
}
return sum;
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("%d", fib(n));
return 0;
}
1 1 2 3 5 8 13.。。
a b sum
算法的思想如代码所示
当n<=2时,便会直接返回sum=1
当n>2时,sum=a+b,同时b会把值赋给a,sum会把值赋给b.
1、许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2、但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3、当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
汉诺塔问题:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int i = 0;
void move(int n, char from, char to)
{
printf(“第%d步:把%d号盘子从%c---->%c\n”, i++, n, from, to);
}
void Hanoi(int n,char start,char tran,char end)
{
if (n == 1)
{
move(n, start, end);
}
else
{
Hanoi(n - 1, start, end, tran);
move(n, start, end);
Hanoi(n - 1, tran, start, end);
}
}
int main()
{
int n = 0;
while ((scanf("%d", &n)) != EOF)
{
i = 1;
Hanoi(n, ‘1’, ‘2’, ‘3’);
printf(“最后总步数为:%d\n”, i - 1);
}
return 0;
}
这个问题的主要核心是我们要理解这些盘子如何移动,之后便可以使用递归来解决问题。
第一步:我们要把n-1个盘子从起始点全部放到缓冲点。(此时我们需要把终点当作缓冲区)
第二步:把最后一个大盘子从起始点放到终点。
第三步:我们要把n-1个盘子从缓冲点全部放到终点。(此时我们需要把起始点当作缓冲区)
只要理解这三步,我们便可以利用递归来写出代码,毕竟后面的复杂运算交给电脑就行!
A (起始点) start B(缓冲点) tran C(终点) end
第一步:我们要把n-1个盘子从起始点全部放到缓冲点的话,代码便可以这样写:
Hanoi(n-1,start,end,tran);(此时我们是把缓冲点当作这个过程的终点)。
第二步:我们要把此时最大的盘子从起始点放到终点,代码便可以这样写:
move(n,start,end); (此时不需要递归,直接调用move打印)。
第三步:我们要把n-1个盘子从缓冲点全部放到终点的话,代码便可以这样写:
Hanoi(n-1,tran,start,end);(此时我们是把缓冲点当作起始点,起始点当作缓冲点的)。
如果自己不信的话,可以把n设为2,自己在纸上推敲一下,看看思想是不是跟我说的一致。
青蛙跳台阶:
1、一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
递归代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int tread(int n)
{
if (n == 0)
{
return 0;
}
else if (n == 1)
{
return 1;
}
else if (n == 2)
{
return 2;
}
else
{
return tread(n - 1) + tread(n - 2);
}
}
int main()
{
int n = 0;
printf(“请输入青蛙跳上n级台阶\n”);
scanf("%d", &n);
printf(“一共有%d步\n”, tread(n));
return 0;
}
循环代码(迭代代码):
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int tread(int n)
{
if (n == 0)
{
return 0;
}
else if (n == 1)
{
return 1;
}
int a = 1;
int b = 2;
int sum = 2;
while (n > 2)
{
sum = a + b;
a = b;
b = sum;
n–;
}
return sum;
}
int main()
{
int n = 0;
printf(“请输入青蛙跳上n级台阶\n”);
scanf("%d", &n);
printf(“一共有%d步\n”, tread(n));
return 0;
}
2、一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
递归代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int tread(int n)
{
if (n == 0)
{
return 0;
}
else if (n == 1)
{
return 1;
}
else
{
return 2 * tread(n - 1);
}
}
int main()
{
int n = 0;
printf(“请输入青蛙跳上n级台阶\n”);
scanf("%d", &n);
printf(“一共有%d步\n”, tread(n));
return 0;
}
3、一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上m级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
1、当n>m时,f(n)=2f(n-1)-f(n-1-m);
2、当n<=m时,f(n)=2f(n-1);
递归代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int tread(int n,int m)
{
if (n > m)
{
if (n == 0)
{
return 0;
}
else if (n == 1)
{
return 1;
}
else
{
return 2 * tread(n - 1,m) - tread(n - 1 - m,m);
}
}
else
{
if (n == 0)
{
return 0;
}
else if (n == 1)
{
return 1;
}
else
{
return 2 * tread(n - 1,m);
}
}
}
int main()
{
int n = 0, m = 0;
printf(“请输入青蛙可以跳上m级台阶,以及设定跳上n级台阶\n”);
scanf("%d%d", &m,&n);
printf(“一共有%d步\n”, tread(n,m));
return 0;
}