函数递归
这是本章的重点内容
目录
一.什么是递归?
大师 L. Peter Deutsch 说过:To Iterate is Human, to Recurse, Divine.中文译为:
人理解迭代,神理解递归。
人理解迭代,神理解递归。毋庸置疑地,递归确实是一个奇妙的思维方式。
简单理解递归与循环:
- 递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。
- 循环:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。
递归的内涵
1、定义 (什么是递归?)
在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:递 和 归,这正是递归思想的精华所在。
2、递归思想的内涵(递归的精髓是什么?)
正如上面所描述的场景,递归就是有去(递去)有回(归来),如下图所示。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决,就像上面例子中的钥匙可以打开后面所有门上的锁一样;“有回”是指 : 这些问题的,演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。
毋庸置疑地,递归确实是一个奇妙的思维方式。对一些简单的递归问题,我们总是惊叹于递归描述问题的能力和编写代码的简洁,但要想真正领悟递归的精髓、灵活地运用递归思想来解决问题却并不是一件容易的事情。
递归的作用
- 它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略
- 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
二.两个例子帮助我们理解递归函数
这里我们举一个具体的例子
接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:123,输出 1 2 3
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//123
print(num);
}
该怎么理解呢?我们画个图帮助理解
注意递归的归 :从哪里调用,就回到哪里去,然后继续执行返回处下面的语句
或许,下面这张图能帮助我们理解递归(图片来自网上)
递归的两个必要条件:
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
我们在用一个代码来加深我们对递归的理解:
编写函数,不允许创建临时变量,求字符串的长度。
int my_strlen(char*s)
{
if (*s == '\0')
return 0;
else
return 1 + my_strlen(s + 1);
}
int main()
{
char arr[] = "abc";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
这里我们需要求字符串"arr"的长度
首先需要说明的是arr是数组名,数组名是数组首元素的地址
所以函数my_strlen里的参数应是 char*s
接着,我们再来分析一下这里的递归是怎么用的:
该开始*s指向的字符是a,如果想要*s指向b就要s+1
为什么是+1呢?因为我们这是char*的指针,char*的指针+1就会跳一个字符
如果是int*的指针要+4,因为char*+1我们想要让它跳过一个字符,一个字符是1个字节,一个整型是4个字节,一个double型的指针要跳过一个8
下面用一张图给出我们的思路,我们用这样的递归方式、
这里我们还是用一张图帮助我们理解,红色的线代表递,绿色的线是归 ,相信通过这张图就可以理解递归函数在这道题的运用了
下面我们再来看几道题目
三.求n的阶乘
求n的阶乘这道题目在之前我们已经见识过,但今天我们要用递归的方式实现它
首先我们分析一下:
n!=1*2*3*4*5*6*...*n
(n-1)!=1*2*3*4*...*(n-1)
所以我们可以得到 :n!=n*(n-1)!
我们便可以得到函数f(x)的表达式
下面我们用函数的递归来实现
int fac(int n)
{
if (n <= 1)
return 1;
else
return n * fac(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fac(n);
printf("%d\n", ret);
return 0;
}
四.求第n个斐波那契数。
我们知道斐波那契数列是 1,1,2,3,5,8,13,21,34,55...
我们可以总结一下它的规律,
当n<=2时,为1
当n>2时,为前两项之和
我们开始用函数的递归写代码
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 2) + fib(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d\n", ret);
return 0;
}
这样我们就完成了这道题目,但是我们发现有问题:
- 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
- 使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
为什么呢?
我们发现 fib 函数在调用的过程中很多计算其实在一直重复。
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
每一次调用函数都会在内存的栈区申请一块空间
当我们一直不停地开辟空间,总有一天,栈空间的内存耗光了,没有空间可以开辟了,
这个时候就会出现 栈溢出(stackoverflow)的问题
那么我们应该如何解决这个问题呢?
1. 将递归改写成非递归。
2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不 仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保 存递归调用的中间状态,并且可为 各个调用层所访问。
比如,下面代码就采用了,非递归的方式来实现:
这是我们的分析,我们通过这样的方式不断地改变a和b,从而得到c
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;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d\n", ret);
return 0;
}
提示:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
五.递归的几个练习
1.写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和
#include<stdio.h>
//写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和
int DigitSum(int n)
{
if (n > 9)
{
return DigitSum(n / 10) + n % 10;
}
else
return n;
}
int main()
{
int n = 0;
scanf("%d", &n);
int sum= DigitSum(n);
printf("%d", sum);
return 0;
}
2.递归实现n的k次方
#include<stdio.h>
double Pow(int n, int k)
{
if (k == 0)
{
return 1;
}
else if (k >= 1)
{
return n * Pow(n, k - 1);
}
else
return 1.0 / Pow(n, -k);
}
int main()
{
int n = 0;
int k = 0;
scanf("%d %d", &n, &k);
double ret = Pow(n, k);
printf("%lf\n", ret);
return 0;
}
3.编写一个函数 reverse_string(char * string)(递归实现)
实现:将参数字符串中的字符反向排列,不是逆序打印。
要求:不能使用C函数库中的字符串操作函数。
比如:
char arr[] = "abcdef";
逆序之后数组的内容变成:fedcba
#include<stdio.h>
//递归版本
int my_strlen(char*s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
void reverse_string(char*arr)
{
int len = my_strlen(arr);
char tmp = *arr;
*arr = *(arr + len - 1); //把g放到a的位置上
*(arr + len - 1) = '\0';
if (my_strlen(arr + 1) > 1)
{
reverse_string(arr + 1);
}
*(arr + len - 1) = tmp;
}
int main()
{
char arr[] = "abcdefg";
reverse_string(arr);
printf("%s", arr);
}