数据结构 —— 递归专题

133 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

递归专题

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 每个数都是前两项相加。