本文已参与「新人创作礼」活动,一起开启掘金创作之路
递归专题
1、前景知识
递归定义:不同函数之间的,相互调用。
#include <stdio.h>
void f();
void g();
void k();
void f()
{
printf("FFFF\n");
g();
printf("1111\n");
}
void g()
{
printf("GGGG\n");
k();
printf("2222\n");
}
void k()
{
printf("KKKK\n");
}
int main(void)
{
f();
return 0;
}
自己调用自己
1、死递归:不知道什么停止调用自己 2、递归:一定要知道,自己什么时候停止调用自己。
#include <stdio.h>
// 死递归
void f()
{
printf("1111\n");
}
int main(void)
{
f();
return 0;
}
// 不是死递归
void f(int n)
{
if(n==1)
printf("1111\n");
else
f(n-1);
}
int main(void)
{
f();
return 0;
}
2、应用举例
(1)求阶乘
n! = n x (n-1)!
(1)使用循环来实现
#include <stdio.h>
int main(void)
{
int val;
int i=0;
int mul = 1;
printf("请输入一个数字:val = ");
scanf("%d ",&val);
for(i=1, i<=val; i++)
{
mul = i * mul;
}
printf("%d 的阶乘是 %d\n", val, mul);
return 0;
}
(2)使用递归来实现
#include <stdio.h>
// 1、出入一个数
// 2、返回这个数的阶乘
long f(long val)
{
// f(1) 肯定可以实现
if(1==n)
return 1;
// f(n) 要借助 f(n-1) 来实现
else
return f(n-1) * n;
}
int main(void)
{
printf("%d \n",f(5));
return 0;
}
递归的思想: 规模很大的问题的解决,是借助于规模很小问题解决,当最后规模最小的问题,不需要再借助其他解决办法的时候,再倒推回来。
求:100! -> 99! ->98! ...-> ... ...-> 1! 所以,先求 1!,然后再反推。
1、
n规模问题的解决,可以借助(n-1)规模问题的解决而解决。 举例:求100!,我们知道99!,就可以轻易的得到100!
(2)求 1+2+3+4+....+100
//
long f(long n)
{
// 规模最小的时候 n==1
if(1==n)
return 1;
// 剩下的规模,需要借助 n-1 的规模来解决
else
return f(n-1) + n;
}
int main(void)
{
printf("%d \n",f(5));
return 0;
}
3、对递归的理解
定义:一个函数,直接或者间接,调用自己。
(1)函数调用:
当在一个函数的运行期间调用另一个函数时,在运行被调函数之前,系统需要完成三件事:
1、将所有的实际参数、返回地址等信息传递给被调函数保存。 返回地址:下一条语句的地址
2、为被调函数的局部变量(也包括行参)分配存储空间。
3、将控制转移到被调函数的入口。
从被调函数返回函数之前,系统也要完成三件事:
保存被调函数的返回结果。 保存
return的值
释放被调函数所占的存储空间。
依照被调函数保存的返回地址将控制转移到调用函数。
举例1:
#include <stdio.h>
int f(int n)
{
int i,j;
n = n+2;
return n;
}
// f 返回函数,需要做的事情
1、保存 n 的值
2、释放所有形参、局部变量,
3、根据保存地址,返回主调函数
int main(void)
{
int val;
val = f(5);
printf("val = %d\n",val);
return 0;
}
// 在 main 函数当中调用 f() 函数
1、将实参 5 ,下一个语句地址 printf 的地址,
2、为形参 n 分配空间
3、控制权限转移
A函数调用A函数,和A函数调用B函数,在计算机看来是没有任何区别的,只不过用我们日常的思维方式理解比较怪异而已!
(2)函数调用,栈的使用
当有多个函数相互调用时,按照”后调用先返回“的原则,上述函数之间信息传递和控制转移必须借助”栈“来实现。
即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就在栈顶分配一个存储区,进行压栈操作,每当一个函数退出时,就释放它的存储区,就做出栈操作,当前运行的函数永远都在栈顶位置。
(3)递归调用,必须满足的3个条件
-
递归必须得有一个明确的中止条件
-
该函数所处理的数据规模必须在递减(递归的值,可以递增)
// 值在减小、规模也在减小
int f(int n)
{
if(n<3)
printf("结束\n"); // 递归结束
else
n = f(n-1); // 在此递归
return n;
}
// 值在增大、规模却在减小
int f(int n)
{
if(n>7)
printf("结束\n"); // 递归结束
else
n = f(n+1); // 在此递归
return n;
}
- 这个转化必须是可解的
(4)循环的递归的关系
(1)理论上讲,所有的循环都可以转化为递归。但是用递归能解决的问题,不一定用循环可以解决。
(2)递归和循环的特点
递归:易于理解、速度比较慢、占用存储空间大 优点:易于理解 缺点:调用函数,有很大的开销。
循环:不易理解、速度比较快、占用存储空间比较小
(5)汉诺塔
如下图所示,从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面。
求移动的步骤和移动的次数
解:
(1)
n == 1第1次 1号盘 A---->C sum = 1 次
(2)
n == 2第1次 1号盘 A---->B 第2次 2号盘 A---->C 第3次 1号盘 B---->C sum = 3 次
(3)
n == 3第1次 1号盘 A---->C 第2次 2号盘 A---->B 第3次 1号盘 C---->B 第4次 3号盘 A---->C 第5次 1号盘 B---->A 第6次 2号盘 B---->C 第7次 1号盘 A---->C sum = 7 次
n=1; 1次 n=2; 3次 n=3; 7次 总结:一共是 2 的 n次方,减一。
// 当用户输入盘子个数的时候,我们就假设 A 上有n个盘子,并且从小到大排列好了
void hannuota(int n, char A, char B, char C)
{
如果是一个盘子,
直接将 A 柱子上的盘子移动到 C柱子上面
否则
先将 A 柱子上的 n-1 个盘子借助 C 移动到B上面
直接将 A 柱子上的第 n 个盘子移动到上C上面
最后将 B 柱子上面的n-1个盘子借助 A 移动到 C 上
}
// n:代表要移动盘子的总数
// A:代表准备要移动的柱子,(不一定是 A ,有可能是 B)
// B:代表移动过程中借助的柱子(不一定是 B,有可能是 A)
// C:代表要接收的盘子的柱子,(也不一定是C)
void hannuota(int n, char A, char B, char C)
{
if(1==n)
{
printf("将编号为 n 的柱子,直接从 %c 柱子,移动到 %c的柱子上面\n",n,A,C);
}
else
{
//将 A 上面的 n-1 个盘子,借助 C 移动到 B 上面.
hannuota(n-1,A,C,B);
//将编号为 n 的盘子,移动到 C 上面
printf("将编号为 n 的柱子,直接从 %c 柱子,移动到 %c的柱子上面\n",n,A,C);
//
hannuota(n-1,B,A,C);
}
}
总结:其实从宏观上来说,只需要 3 步(只是其中两步比较复杂)
1、A上的 n-1 个移动到 B 上 (比较复杂) 这个原理和(将 n 个圆盘从 A 移动到 C 是一样的)
2、A上的第 n 个移动到 C 上
3、B 上的 n-1 个移动到 C 上(比较复杂) 这个原理和(将 n 个圆盘从 A 移动到 C 是一样的)
就像将大象放到冰箱里,也需要3步,第二步,将大象放到冰箱比较复杂,但是打开冰箱,和关闭冰箱比较容易。
4、递归的应用
1、树和森林,就是以递归的方式来定义的。 2、树和图,很多算法就是以递归来实现的。 3、很多数学公式,就是以递归的方式来定义的。
斐波那契数列:1、2、3、5、8、13、21、34 每个数都是前两项相加。