函数 —— 下

37 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情

一、函数递归

1. 什么是递归

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

2. 递归的三个必要条件

1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。 2.每次递归调用越来越接近这个限制条件 3.递归的层次不能太深(补充)


这是一个错误的递归,因为它缺少递归的两个必要条件。但是它确实是自己调用了自己

#include<stdio.h>
int main()
{
	printf("hehe\n");
	main();
	return 0;
}

运行现象:死循环? - NO~ 发现在运行时,过了会它停下来了

经调试发现 -> Stack overflow:栈溢出 在这里插入图片描述 这里有一个程序猿的知乎网站 stackoverflow.com/


3.通过练习了解递归

1.接收一个整形值(无符号),按照顺序打印它的每一位。如:1234,输出1 2 3 4

1: 逆序输出:

1234 / 10^0 % 10 = 4 1234 / 10^1 % 10 = 3 1234 / 10^2 % 10 = 2 1234 / 10^3 % 10 = 1 -----------------------------------------------------------------  顺序输出: 1234 / 10^3 % 10 = 1 1234 / 10^2 % 10 = 2 1234 / 10^1 % 10 = 3 1234 / 10^0 % 10 = 4

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<math.h>
int main()
{
	unsigned int num = 0;//存放输入的数
	scanf("%u", &num);
	int temp = num;//拷贝这个数
	int count = 0;//计算这个数有几位

	//判断输入的数有几位
	while(temp)
	{
		temp /= 10;
		count ++;
	}
	//顺序求每位数
	int i = 0;
	for(i = count - 1; i >= 0; i--)
	{
		//int ret = pow(10,i);
		//printf("%d ", num / ret % 10);//1 2 3 4
		printf("%d ", num / (int)pow(10, i) % 10);//pow这个函数的返回值和参数类型都是double类型
	}
	return 0;	
}

2:

使用递归的方式 print(1234) print(123) 4 print(12) 3 4 print(1) 2 3 4

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void print(unsigned int n)
{
	//如果是2位数及以上,就要拆分; 
	//满足递归的2个必要条件
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	scanf("%u", &num);
	//递归 - 函数自己调用自己 - print函数可以打印参数部分数字的每一位
	print(num); 
	return 0;
}

这种代码对于初学者不是很友好,但却是程序设计中一个很重要的点,所以看剖析图:

在这里插入图片描述


上面说递归有2个必要条件,那么有这2个条件,递归就一定对吗?看代码:

void test(int n)
{
	if(n < 10000)//限制条件
	{
		test(n + 1);//且每次越来越接近这个限制条件
	}
}
int main()
{
	test(1);
	return 0;
}

经逐过程调试:又遇到了Stack overflow -> 栈溢出 在这里插入图片描述 要了解什么是栈溢出,就要了解内存 在这里插入图片描述 虽然有2个必要条件,显然这里是栈空间不足,才导致的栈溢出。 没有这2个必要条件一定错,但是同时有也不一定对,所以在上面又补充了1个条件:递归的层次不能太深


2. 编写函数不允许创建临时变量,求字符串长度

在之前有提到在求字符串长度时有1个库函数strlen

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "bit";
	printf("%d\n", strlen(arr));//3
	return 0;
}

模拟实现strlen函数

#include<stdio.h>
int my_strlen(char* str)
{
	int count = 0;//统计字符
	while(*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	char arr[] = "bit";
	printf("%d\n", my_strlen(arr));
	return 0;
}

递归思想: my_strlen ("bit") 1 + my_strlen ("it"); 1 + 1 + my_strlen ("t"); 1 + 1 + 1 + my_strlen (""); 1 + 1 + 1 + 0 = 3;

#include<stdio.h>
int my_strlen(char* str)
{
	if (*str)//限制条件
		return 1 + my_strlen(str + 1);//且每次递归 越来越接近于这个限制条件
		//return 1 + my_strlen(str++);//err;str++和++str这2种写法是错误的,在递归中也不建议这样写
	else
		return 0;//注意这里return 0是必需有的,因为在最后条件不满足时,递归结束,程序回收,返回到上1级的递归时 -> return 1 + my_strlen(str + 1),这里的值是未知的
		//所在在写递归时我们不仅要考虑到递归的3个条件,还要在回收递归时多加考虑
}
int main()
{
	char arr[] = "bit";
	printf("%d\n", my_strlen(arr));
	return 0;
}

为了方面理解,又画了一个剖析图 在这里插入图片描述


二、递归与迭代

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

1.循环也可以被称为迭代

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	int n = 0;
	scanf("%d", &n);
	int i = 0;
	int ret = 1;
	//迭代
	for(i = 1; i <= n; i++)
	{
		ret *= i;
	}	
	printf("%d\n", ret);
	return 0;	
}

2.递归 公式:在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Fac(int n)
{
	if (n > 1)
		return n * Fac(n - 1);
	else
		return 1;
}
int main()
{
	int n = 0 ;
	scanf("%d", &n);
	int ret = Fac(n);
	printf("%d\n", ret);
	return 0;
}

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

1 ,1,2,3,5,8,13,21,34,55 ... 斐波那契数列就是前2个数之和等于第3个数 公式:在这里插入图片描述

1:递归

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Fib(int n)
{
	if(n > 2)
		return Fib(n - 1) + Fib(n - 2);
	else
		return 1;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("第%d个斐波那契数是%d\n", n, ret);
	return 0;
}

这里当我们想计算出第50位的斐波那契数的时候。算了我等了许久也没等出来(大概需要5-10分钟左右),难道计算机偷懒了吗?所以这里计算44,方便能观察结果。

在这里插入图片描述

在这里插入图片描述


为了证明计算机没有偷懒 这里我们看一下在计算第40个斐波那契数时需要计算多少次第3个斐波那契数

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int count = 0;
int Fib(int n)
{
	//统计第3个斐波那契数的计算次数
	if(3 == n)
		count++;
	if(n > 2)
		return Fib(n - 1) + Fib(n - 2);
	else
		return 1;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("第%d个斐波那契数是%d\n", n, ret);
	printf("在计算第40个斐波那契数时,第3个斐波那契数需要被计算%d次\n", count);
	return 0;
}

在这里插入图片描述 显然不是计算机偷懒,而是代码效率太低 - 重复大量的计算 可见,只要栈不溢出,递归都可以求解,但是效率太低


#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int Fib(int n)
{
	if(n > 2)
		return Fib(n - 1) + Fib(n - 2);
	else
		return 1;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("第%d个斐波那契数是%d\n", n, ret);
	return 0;
}

2:优化 - 使用循环的方式

前面使用递归求斐波那契数时,是从后往前的,它要一层层的展开,导致重复计算;如果从前往后的不断累加上去,相比效率就要高的多。 具体思路:如果是第2个斐波那契数以下的直接打印;如果是第2个斐波那契数以上的:先拷贝前2个斐波那契数f1、f2,在求第n个斐波那契数时,不断让f1和f2累计运算,得到后面1个数的值,直到第n位斐波那契数

for循环版本:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	int i = 0;
	int f1 = 1;
	int f2 = 1;
	int n = 0;
	scanf("%d", &n);
	if (n < 3)
		printf("第%d个斐波那契数是%d\n", n, f1);
	else
	{
			for(i = 3; i <= n; i++)
			{
				f2 = f1 + f2;
				f1 = f2 - f1;
			}
			printf("第%d个斐波那契数是%d\n", n, f2);
	}
}	

while循环版本:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	int f1 = 1;
	int f2 = 1;
	int f3 = 1;//将结果设置为1,在求<3的斐波那契数时直接输出1
	int n = 0;
	scanf("%d", &n);
	int u  = n;//拷贝1份,因为在while循环里要改变n的值,不方便在下面的输出
	while(u > 2)
	{
		f3 = f1 + f2;
		f1 = f2;
		f2 = f3;
		u--;
	}
	printf("第%d个斐波那契数是%d\n", n, f3);
	return 0;
}

接着我们想来看一下循环版本第50位斐波那契数是多少 ? 在这里插入图片描述 效率非常快,但是显然这个数已经已经不能被int类型所存储了。

最后在这里想说的是递归并不一定适于所有代码,使用递归可能会写出栈溢出、和效率低等 问题。所以在以后的编码中,要知道哪种方法更适用。