递归函数(2)&递推算法

小标题:01|递归函数(2)|递归函数的缺陷|引出递推算法

本篇简介

本篇文章和视频主要介绍了递归函数的缺陷和递推算法

视频

引入

大家好,欢迎来到《从0开始的C++算法课》的第1期,

本期将带大家发现递归函数的弊端,进一步理解递归,并引出递推算法来弥补递归的缺陷。

兔子数列问题

有一对兔子,从出生之后的第3个月起每个月都生一对兔子,一对小兔子成长到第3个月后每个月又生一对兔子,假如兔子都不死,问第n个月(n<=50)的兔子总数为多少对?

解析

这是一个非常经典的问题,使用上个视频的知识点递归即可求解。

废话不多说来分析一下:

  • 我们把第1个月的这俩只兔子记为 a1 和 a2,此时兔子对数是 1 对。

  • 第2个月的时候,兔子依旧是 a1 和 a2,兔子对数依旧是 1 对。

  • 第3个月的时候,除了a1和a2之外,a1和a2 还会生一对兔子,可以记为 b1, b2,此时兔子对数是 2 对。

  • 第4个月的时候,除了a1、a2、b1、b2之外,注意从第3个月起兔子每个月都会生一对兔子,所以a1和a2还会再生一对兔子,我们记为 c1, c2,此时兔子对数是 3 对。

  • 第5个月的时候,除了a1、a2、b1、b2、c1、c2之外,a1和a2会生一对兔子 d1, d2,这个月也恰好是b1、b2出生后的第三个月,因此也会生一对兔子,记为e1 和 e2,此时兔子对数是 5 对。

以此类推...

月份兔子兔子对数
1a1, a21
2a1, a21
3a1, a2, b1, b22
4a1, a2, b1, b2, c1, c23
5a1, a2, b1, b2, c1, c2, d1, d2, e1,e25

其实我们只需要关注每个月有多少对兔子就可以了,得到这样一组数据:

月份12345
兔子对数11235

请发现它们之间的规律,计算出第6个月有几对兔子?

没错,第6个月的兔子对数是 8 ,怎么来的呢?

第一个数 1 和第二个数 1 相加,得到的是第三个数 2,第二个数 1 和第三个数 2 相加,得到第四个数 3,同理,第五个数 5 是它前面的两个数 2 和 3 相加。

这个规律显而易见是当前项的值等于前一项的值加上前前项的值。

可以把它记作:

A(n) = A(n-1) + A(n-2)

当前项 = 前一项 + 前前项

image-20240422170047578

斐波那契数列

熟悉数列的朋友一定发现了,它其实是斐波那契数列,也可以叫做兔子数列。

接下来我们来使用递归的方式求解这个问题:

月份12345...n-2n-1n
兔子对数11235...

创建函数之前,首先确定函数是否有返回值,如果有返回值,返回值的类型是什么?

这个问题最终是需要返回第n个月的兔子对数?通过这一点可以确定函数有整型返回值,且有整型参数。

当函数有返回值的时候,先把返回值在函数内部写好,以免忘记,这是一个比较好的编程习惯。

#include <iostream>
using namespace std;

int f()  // 创建函数时先思考函数返回值
{
    int res;  // 这个函数需要有整型返回值,因此创建一个整型变量并返回
    return res;	// 返回整型变量
} 

int main()
{
    return 0;
}

函数返回的是数列中第n项的结果,根据规律,第n项的值是它前一项和前前项的值的和。这个函数就是用来求解数列某一个项的值的,因此前一项和前前项的值可以表达为f(n-1) 和 f(n-2)。

因此:res = f(n-1) + f(n-2)

#include <iostream>
using namespace std;

int f(int n)
{
    int res;
    res = f(n-1) + f(n-2);

    return res;	
} 

int main()
{
    return 0;
}

此时递归函数构建完了吗?

没有!,递归函数需要有起始项,避免无限制递归,对于这个数列来说,起始项应该有两项,分别是第1项和第2项,因为只有知道了两项才能推算出接下来一项的结果。

在代码中补充递归的起始项,当 n == 1 或者 n==2 的时候,结果都是 1,else里把刚才的规律代码填入即可。

#include <iostream>
using namespace std;

int f(int n)
{
    int res;
    if (n==1 || n==2)
    {
        res=1;
    }
    else
    {
        res = f(n-1) + f(n-2);
    }

    return res;	
} 


int main()
{
    int ret = f(6);
    cout << ret << endl;
    return 0;
}

把代码放到编程环境中进行测试:

结果正确!

我们也可以使用 for 循环将每一项都输出查看一下结果,结果依旧正确。

#include <iostream>
using namespace std;

int f(int n)
{
    int res;

    if (n==1 || n==2)
    {
        res=1;
    }
    else
    {
        res = f(n-1) + f(n-2);
    }

    return res;	
} 


int main()
{
    for (int i=1; i<=6; i++)
        {
            cout << i << ":" << f(i) << endl;
        }

    return 0;
}

不知道你有没有注意到题目中的这个提示:

image-20240422172952680

n<=50,题目中为什么要有这样的一条限制呢?

我们在程序中修改 for 循环次数为 50 次,运行程序,观察一下结果:

#include <iostream>
using namespace std;

int f(int n)
{
    int res;

    if (n==1 || n==2)
    {
        res=1;
    }
    else
    {
        res = f(n-1) + f(n-2);
    }

    return res;	
} 


int main()
{
    for (int i=1; i<=50; i++)  // 修改此处为50
        {
            cout << i << ":" << f(i) << endl;
        }

    return 0;
}

一开始一瞬间就算出了前面的数据,但是越往后越慢,甚至等了半天也不见出结果,这就是递归的缺陷了,我们来具体分析一下为什么会导致这样的结果?

为了方便画图,这里以求解第6项举例

  • 求解第 6 项,n = 6 ,则结果是f(6),

  • 想要求出 f(6) ,则需要知道它的前两项,即f(5) 和 f(4)

  • 想要求出 f(5) ,则需要知道它的前两项,即f(4) 和 f(3)

  • 想要求出 f(4) ,在需要知道它的前两项,即f(3) 和 f(2)

  • 左侧这里也有 f(4),同样要知道 f(3) 和 f(2)。

此时应该已经有人发现问题了,在递归函数求解的过程中,很多项都在重复计算着。

这也就自然造成了,求解的项数越多执行效率越低。

image-20240422170323985

那么如何解决这个问题呢?

不卖关子,答案是:不使用递归

我们可以使用for循环将的每个月的兔子对数计算出来并记录在数组中。

需要注意的是,数组的下标不是从1开始的,而是从0开始的,为了将代码和实际使用之间映射得更加易懂,我们可以在使用数组的时候不使用下标为 0 的这个位置,这样就可以用下标来代表月份。

月份123...n-2n-1n
兔子对数112...
数组元素(兔子对数)112...n-2n-1n
数组下标012...
数组元素(兔子对数)112...n-2n-1n
数组下标0123...

接下来就是编写代码了

首先创建一个整数数组,

int a[];

#include <iostream>
using namespace std;

int main()
{
	int a[];
	
	return 0;
}

数组的长度可以由输入来决定,也可以预估一个长度,比如这里我们想要计算第50个月的兔子对数,那么可以将数组的长度定为60,

int a[60];

#include <iostream>
using namespace std;

int main()
{
	int a[60];
	
	return 0;
}

稍微定义得比实际要使用的长度长一丢丢,防止太短不够用,定义太长又会浪费,因此多一丢丢丢就可以了。这种预估数组长度的方式是解题中比较常见的。

在主函数main中,只定义数组不赋值的情况下,数组中的所有元素是随机的,为了避免错误,将其初始化为 0 。

int a[60] = {0};

#include <iostream>
using namespace std;

int main()
{
	int a[60] = {0};
	
	return 0;
}

前两个月的兔子对数同样是需要初始化说明的,将它们存储在数组下标为1和下标为2的位置。

#include <iostream>
using namespace std;

int main()
{
	int a[60] = {0};
	
	a[1] = 1;
	a[2] = 1;
	
	return 0;
}

接下来使用 for 循环求解。

前两个月的兔子对数已经初始化,因此只需要从第三个月开始循环求解 i=3。我们要求解第 50 个月,因此循环到数组下标为 50 的位置 i<=50。

#include <iostream>
using namespace std;

int main()
{
	int a[60] = {0};
	
	a[1] = 1;
	a[2] = 1;
	
	for (int i=3; i<=50; i++)
	{
		
	} 
	
	return 0;
}

每个月的兔子对数求解结果需要存储在数组对应的位置,

a[i];

根据兔子数列的规律,当前项的值是前一项的值加上前前项的值,所以是

a[i] = a[i-1] + a[i-2];

#include <iostream>
using namespace std;

int main()
{
	int a[60] = {0};
	
	a[1] = 1;
	a[2] = 1;
	
	for (int i=3; i<=50; i++)
	{
		a[i] = a[i-1] + a[i-2];	
	} 
	
	return 0;
}

代码完成,在编辑器中运行测试一下~

依旧使用 for 循环将每个月以及每个月对应的兔子对数输出出来查看结果,这样做可以和之前递归方式求解速度进行对比

#include <iostream>
using namespace std;

int main()
{
	int a[60] = {0};
	
	a[1] = 1;
	a[2] = 1;
	
	for (int i=3; i<=50; i++)
	{
		a[i] = a[i-1] + a[i-2];	
	} 
	
	for (int i=1; i<=50; i++)
    {
        cout << i << ":" << a[i] << endl;
    }
	
	return 0;
}

会发现一瞬间就计算出了所有的结果,很直观。但是!求解的结果正确吗?

结果中出现了负数,显然是不对的,这个问题和代码中使用的数据类型有关系,我们知道不同的数据类型都有自己的存储范围,因为求解出的结果太大,int 类型已经不足以存储了,所以导致了这个错误。可以修改一下代码,将 int 类型换成 long long 。

#include <iostream>
using namespace std;

int main()
{
	long long a[60] = {0};
	
	a[1] = 1;
	a[2] = 1;
	
	for (int i=3; i<=50; i++)
	{
		a[i] = a[i-1] + a[i-2];	
	} 
	
	for (int i=1; i<=50; i++)
    {
        cout << i << ":" << a[i] << endl;
    }
	
	return 0;
}

此时再运行代码,结果正确。

并且是瞬间就计算出了所有的结果,这是因为每个月的计算结果都被存储到了数组中,并没有重复计算的部分,这是优于递归的。

递推

这种方式我们称为递推,递推是一种用若干可重复运算来描述复杂问题的方法。简单粗暴的理解就是找规律。

这个规律可以称为递推式

实际举例说明递推

看下面这道题:

在墙角按照规律堆放着一堆完全相同的正方体小块儿,只需要知道层数就可以计算所有小块儿的数量。

要求:

输入:一个整数 n ,代表层数(1<=n<=100)

输出:一个整数,表示这堆小块儿的总量。

image-20240422181316149

可以先找一找规律

相信很多人都找到了规律,让我浅浅猜测一下,你们有没有人在找规律的时候是把每一层的数量都算出来的,

题目解析

其实并不需要去思考每一层有多少个小块儿,我们只需要找到每一层之间的变化规律就可以了,从上往下看,总共有五层,第一层是1个小块儿,第二层比第一层多了2个小块儿,第三层比第二层多了3个小块儿,第四层比第三层多了4个小块儿,第五层比第四层多了5个小块儿

这里的规律就是:除第一层外,每一层比上一层多了层数量的木块儿。

那么递推式就可以写作:A(n) = A(n)-1 + n

n代表层数,也就是当前层是上一层加上当前层的层数。

编写代码:

首先需要输入要求解的层数,记为 n ,

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
	
    return 0;
}

把第一层的小块儿数量作为递推的开始,需要先设置,创建一个变量来存储,初始化为1,

int level = 1;

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
	int level = 1;
    
    return 0;
}

接着写 for 循环,从第二层开始计算即可,所以 for 循环从 2 开始,总共到第 n 层,要包含 n ,

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
	int level = 1;
    
    for (int i=2; i<=n; i++)
        {
            
        }
    
    return 0;
}

level 代表每一层小方块的数量,for 循环中的 i 动态表达了层数,根据递推式代码可以写作

level = level + i;

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
	int level = 1;
    
    for (int i=2; i<=n; i++)
        {
            level = level + i;
        }
    return 0;
}

由于这道题是求出所有小方块儿的数量,所以我们需要再做一个累加

新建一个变量用来存储总数,因为 for 循环是从第二层开始计算的,所以总数初始化为 1,也就是把第一层的数量先初始化在总数中。

int sum=1;

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
	int level = 1;
    
    int sum = 1;
    for (int i=2; i<=n; i++)
        {
            level = level + i;
        }
    
    return 0;
}

在 for 循环中补充累加的代码

sum = sum + level;

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
	int level = 1;
    
    int sum = 1;
    for (int i=2; i<=n; i++)
        {
            level = level + i;
            sum = sum + level;
        }
    
    return 0;
}

最后输出 sum

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
	int level = 1;
    
    int sum = 1;
    for (int i=2; i<=n; i++)
        {
            level = level + i;
            sum = sum + level;
        }
    cout << sum;
}

在编辑器中运行代码测试,测试结果正确

关于 for 循环的使用以及累加比较简单,在编写代码时没有做出非常详细的解释,如果在这里有不理解的地方,可以去补充一下 for 循环的相关基础知识。

练习题

和上期一样,这里也给出两道题,供大家练习

第一题

名称:猴子吃桃子

描述:猴子第一天摘下若干个桃子,当即吃了一半还不过瘾,又多吃了一个,第二天又将剩下的桃子吃掉一半又多吃了一个,以后每天早上都吃了前一天剩下的一半零一个。到了第十天想再吃时,见只剩下一个桃子,求第一天共摘了多少个桃子?

输入:无

输出:一个整数,第一天共有多少个桃子

第二题

描述:求1/1 + 1/2 + 2/3 + 3/5 + 5/8 + 8/13 + 13/21 + 21/34......的前n项的和

输入:

第1行:一个整数 n (1<=n<=30)

输出:

一行:一个小数,即前 n 项之和(保留3位小数)

样例输入:20

样例输出:12.660

提示一下,第2道题可以结合递归和递推两种方式共同求解。

好了以上就是本期的所有内容了,如果觉得对你有所帮助的话,欢迎关注~

Last Updated:
Contributors: leemiao