【C语言】一文带你了解函数递归及经典案例

纸上得来终觉浅, 绝知此事要躬行。
主页:June-Frost
专栏:C语言

该篇将带你了解 递归知识

目录:

    • 认识递归
    • 练习:
      • 按顺序打印每一位数字
        • 递归的必要条件
      • 模拟实现strlen
    • 递归与迭代:
        • 求n的阶乘(不考虑溢出)
        • 求第n个斐波那契数(不考虑溢出)
    • 经典递归问题
      • 汉诺塔问题
      • 青蛙跳台阶问题
    • ❤️ 结语

认识递归

 程序调用自身的编程技巧称为递归( recursion)。简单的来说就是函数自己调用自己。

 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的精髓在大事化小

下面将通过例子(递归和非递归)来帮助理解函数递归的思考方式。


练习:

按顺序打印每一位数字

接受一个无符号整型值,按照顺序打印它的每一位:
输入 :1234 打印:1 2 3 4


非递归:这个题很容易想到的一种思路就是放在数组中解决⟹
【C语言】一文带你了解函数递归及经典案例_第1张图片
代码实现:

#include
int main()
{
	unsigned int num = 0;
	scanf("%d", &num);
	int arr[100] = {0};
	int count = 0;
	//填充数组
	while (num != 0)
	{
		arr[count] = num % 10;
		num /= 10;
		count++;
	}
	//打印
	int i = 0;
	for (i = count-1; i >= 0; i--)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

非递归的方式还是有些繁琐,我们尝试用更加简单的方法继续实现这个问题,递归就可以完美解决。


递归:

思路:假设一个函数为print,它的功能为:传入的参数可以按顺序打印每一位。假如我们传参1234,print(1234)就可以将其打印出来,我们将其大事化小:
【C语言】一文带你了解函数递归及经典案例_第2张图片

  • 将 打印 1234 的问题转化为 打印123 和 打印 4 的问题。
  • 将 打印 123 的问题转化为 打印12 和 打印 3 的问题。
  • 将 打印 12 的问题转化为 打印1 和打印 2 的问题。
  • 打印 1 的问题无法继续拆分,那么 递归条件为:两位数以上。1位数则需要单独处理。
  • 这样在处理该问题时只需要在该条件下,将这部分【相似】逻辑写出来即可。

代码实现:

#include
void print(unsigned int num)
{
	if (num > 9)
	{
		print(num / 10);
		printf("%d ", num % 10);
	}
	else
	{
		printf("%d ", num);
	}
}

int main()
{
	unsigned int num = 0;
	scanf("%d", &num);
	print(num);
	return 0;
}

也可以直接写为:

void print(unsigned int num)
{
	if (num > 9)
	{
		print(num / 10);
	}
	printf("%d ", num % 10);
}

该函数如何理解呢❓
【C语言】一文带你了解函数递归及经典案例_第3张图片
该函数在栈区时怎样的❓
下图为函数递归到最后,在栈区的大概样子。
【C语言】一文带你了解函数递归及经典案例_第4张图片
如果想要了解栈帧的相关细节,可以看博主的另一篇 ——> 函数栈帧。

通过这样处理,该问题就可以顺利解决。同时这样的解决方式也反应了递归的必要条件。


递归的必要条件

  • 需要有限制条件,当满足该条件时,递归不再进行。
  • 每次递归调用之后需要越来越接近该条件。

若是无法满足这样的条件,递归很可能会导致栈溢出等一系列问题。


模拟实现strlen

非递归:只需要遍历一遍并计数即可。(需要创建临时变量)

size_t my_strlen(char* p)
{
	int count = 0;//临时变量
	while (*p++)
	{
		count++;
	}
	return count;
}

递归:这种方式不需要创建临时变量。

思路:与上一道题一样,我们假设my_strlen 可以直接计算出字符串的长度。假设传参:”abcd“
【C语言】一文带你了解函数递归及经典案例_第5张图片

  • 将计算 abcd 的长度拆分为 计算 bcd 的长度 后再加 1。
  • 将计算bcd 的长度拆分为 计算 cd 的长度 再加 1。
  • 将计算cd 的长度 拆分为 计算 d 的长度 再加 1 。
  • 计算d 的长度拆分为 计算 空字符串 的长度 再加 1。
  • 空字符串 无法像上面一样拆解,于是单独处理,返回 0 。

代码实现:

size_t my_strlen(char* p)
{
	if (*p)
	{
		return my_strlen(++p) + 1;
	}
	return 0;
}

递归与迭代:

通过以上的例子,友友们了解了递归,但是在一些问题中,我们可以使用递归也可以使用 迭代 (循环就是迭代的一种)。

求n的阶乘(不考虑溢出)

迭代:遍历累积 即可。

int Fun(int num)
{
	int ret = 1;
	int i = 0;
	for (i = 1; i <= num; i++)
	{
		ret *= i;
	}
	return ret;
}

递归:

思路:
【C语言】一文带你了解函数递归及经典案例_第6张图片

代码实现:

int Fun(int num)
{
	if (num > 0)
	{
		return Fun(num - 1) * num;
	}
	return 1;
}

求第n个斐波那契数(不考虑溢出)

迭代:

int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}

递归:
【C语言】一文带你了解函数递归及经典案例_第7张图片
代码实现:

int Fib(int n)
{
	if (n > 2)
	{
		return Fib(n - 1) + Fib(n - 2);
	}
	return 1;
}

注意:

  • 在解决第一个问题时,如果使用递归传参过大,由于系统分配给程序的栈空间时有限的,随着不断创建栈帧,就会导致栈溢出。
  • 在解决第二个问题时,递归的时间复杂度为 O(2^n),这就会导致程序效率低下。

处理方式:

  • 如果问题的解决 可以使用迭代也可以使用递归,并且两者不会出现明显的缺陷,那么使用哪一种都可以。
  • 如果递归的方式有着栈溢出等问题,就需要改为非递归方式解决问题。
  • 使用 static 对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问,如果一个函数的局部变量过多,且递归栈帧需要创建很多时,这样的方式可以减少申请的空间大小,降低栈溢出的风险。

经典递归问题

汉诺塔问题

汉诺塔问题是一个经典的数学问题,起源于印度。问题描述如下:有三个柱子,分别标记为A、B、C,A柱子上有n个不同大小的盘子,盘子按照从小到大的顺序摆放,要求将所有盘子从A柱子上移动到C柱子上,期间可以借助B柱子完成移动。但是在移动过程中,要求大盘子永远不能放在小盘子上面,如何去做?

思路: 递归的使用,其实将问题抽象化。汉诺塔是将n个在a上的盘子通过b移动至c,那么我们假设一个函数为:Hanoi,它的功能就是可以将起始柱上的盘子通过中转柱移位到目的柱。例如:Hanoi(n,a,b,c),就是a作为起始柱,b作为中转柱,c作为目的柱。这里将那么我们将这个汉诺塔问题通过这个函数功能不断拆分。

【C语言】一文带你了解函数递归及经典案例_第8张图片
这样的关系就是递归的核心❗️
【C语言】一文带你了解函数递归及经典案例_第9张图片
其中 将n-1个起始柱上的盘子移位到中转柱 这个步骤可以按照同样的逻辑拆分
【C语言】一文带你了解函数递归及经典案例_第10张图片
对其拆分时,目标柱为第二个柱子。
【C语言】一文带你了解函数递归及经典案例_第11张图片
同样的 将n-1个中转柱的盘子转移到目标柱,也可以按照之前的逻辑拆分,这里,第三个柱子为目标柱,第二个为起始柱子。
【C语言】一文带你了解函数递归及经典案例_第12张图片
在这里我只分析到了第二次拆分,这里可以继续拆分,直到当拆分到只有一个盘子时便无法继续拆分了(没有多余的盘子可以移动),只需要单独处理即可,同时这也是递归停止的条件。

代码实现:

#include
//         数量      起始柱   中转柱    目的柱  
void Hanoi(int n, char pos1, char pos2, char pos3)
{
	if (n > 1)
	{
		Hanoi(n - 1, pos1, pos3, pos2);
		printf("%c->%c ", pos1, pos3);
		Hanoi(n - 1, pos2, pos1, pos3);
	}
	else
	{
		printf("%c->%c ", pos1, pos3);
	}
}
int main()
{
	
	int n = 0;
	scanf("%d", &n);
	Hanoi(n,'A', 'B', 'C');
	return 0;
}

青蛙跳台阶问题

青蛙跳台阶问题是一个经典的递归问题。假设有n 级台阶,一只青蛙可以跳 1级或 2级,问青蛙跳上这 n 级的台阶总共有多少种跳法?

思路: 因为青蛙可以一次跳两个或者一次跳一个,那么计算n级台阶的跳法,只需要知道跳上n-1级的跳法和n-2级的跳法,加起来就可以了,这个问题可以通过这样的逻辑不断拆分,所以使用递归将其解决。
【C语言】一文带你了解函数递归及经典案例_第13张图片
这个问题可以转化为斐波那契数的问题。

代码实现:

#include
int Fun(int n)
{
	if (n > 2)
	{
		return Fun(n - 1) + Fun(n - 2);
	}
	if (n == 2)
	{
		return 2;
	}
	if (n == 1)
	{
		return 1;
	}
}

❤️ 结语

文章到这里就结束了,如果对你有帮助,你的点赞将会是我的最大动力,如果大家有什么问题或者不同的见解,欢迎大家的留言~

你可能感兴趣的:(c语言,c语言,开发语言,函数,递归,汉诺塔)