递推
递推思想跟枚举思想一样,都是接近人类思维方式的思想,甚至在实际生活具有比枚举思想更多的应用场景。人脑在遇到未知的问题时,大多数人第一直觉都会从积累的「先验知识」出发,试图从「已知」推导「未知」,从而解决问题,说服自己。
事实上,这就是一种递推的算法思想。递推思想的核心就是从已知条件出发,逐步推算出问题的解。实现方式很像是初高中时我们的数学考卷上一连串的「因为」「所以」。那个时候还是用三个点来表示的。
而对于计算机而言,复杂的推导其实很难实现。计算机擅长的是执行高密度重复性高的工作,对于随机性高变化多端的问题反而不好计算。相比之下,人脑在对不同维度的问题进行推导时具有更高的自由度。
比方说,人脑可以很容易的从「太阳从东边升起」推出「太阳从西边落下」,然后大致推出「现在的时间」。但是对于计算机而言并没有那么容易,你可能需要设置一系列的限制条件,才能避免计算机推出「太阳/月亮/星星」从「南/北/东边」「落下/飞走/掉落」的可能性。
我说这个例子的用意是在说明,计算机在运用递推思想时,大多都是重复性的推理。比方说,从「今天是1号」推出「明天是2号」。这种推理的结构十分类似,往往可以通过继而往复的推理就可以得到最终的解。
递推思想用图解来表示可以参见下图。每一次推导的结果可以作为下一次推导的的开始,这似乎跟迭代、递归的思想有点类似,不过递推的范畴要更广一些。
应用
兔子问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子假如兔子都不死,问每个月的兔子总数为多少?
解:通过题目,我们可以依次列出前几个月兔子的情况,再找到规律解决问题
月份 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
2 | 0 | 1 | 0 | 1 | 1 | 2 | 3 | 5 | 8 |
3 | 0 | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
total | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 |
通过上述列表我们不难发现,d当计算这个月的兔子时。处在3月的兔子等于上一个月处在3月的兔子 + 上一个月处在2月的兔子;处在2月的兔子就等于上一个月处在1月的兔子;处在1月的兔子就等于处在3月的兔子的个数。
代码:
//有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子
//假如兔子都不死,问每个月的兔子总数为多少?
size_t Rabbit(int count)
{
if (count == 0)
{
return 0;
}
if (count == 1 || count == 2)
{
return 1;
}
int one = 1;
int two = 0;
int three = 0;
size_t total = 0;
while (--count)
{
three += two;
two = one;
one = three;
}
total = one + two + three;
return total;
}
int main()
{
int count = 9;
size_t res = Rabbit(count);
printf("%d个月共可以产%u对兔子\n", count, res);
return 0;
}
运行截图:
递归
完递推,就不得不说说它的兄弟思想——递归算法。二者同样都带有一个「递」字,可以看出二者还是具有一定的相似性的。「递」的理解可以是逐次、逐步。在递推中,是逐次对问题进行推导直到获得最终解。而在递归中,则是逐次回归迭代,直到跳出回归。
递归算法实际上是把问题转化成规模更小的同类子问题,先解决子问题,再通过相同的求解过程逐步解决更高层次的问题,最终获得最终的解。所以相较于递推而言,递归算法的范畴更小,要求子问题跟父问题的结构相同。而递推思想从概念上并没有这样的约束。
用一句话来形容递归算法的实现,就是在函数或者子过程的内部,直接或间接的调用自己算法。所以在实现的过程中,最重要的是确定递归过程终止的条件,也就是迭代过程跳出的条件判断。否则,程序会在自我调用中无限循环,最终导致内存溢出而崩溃。
递归算法的图解可如下图。很明显,递归思想其实就是一个套娃过程。一般官方都是严禁套娃行为的。所以在使用时一定要明确「套娃」举动的停止条件,及时止损。
应用
之所以把这两个算法放在一起,是因为这个两个算法非常类似,这里我们还是通过兔子问题来加深理解
月份 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
2 | 0 | 1 | 0 | 1 | 1 | 2 | 3 | 5 | 8 |
3 | 0 | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
total | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 |
通过这个表格我们还发现一个规律,就是从第3个月起,兔子的总数等于前两个月的兔子总数之和。其实,这就是著名的斐波那契数列。
代码:
size_t Rabbit(int count)
{
if (count == 1 || count == 2)
{
return 1;
}
return Rabbit(count - 1) + Rabbit(count - 2);
}
int main()
{
int count = 9;
size_t res = Rabbit(count);
printf("%d个月共可以产%u对兔子\n", count, res);
return 0;
}
运行结果:
问题
以上两个代码,第二个代码看起来简洁,但是它的缺点也非常明显,那就是会浪费内存空间,调用一次Rabbit函数,就会调用2次Rabbit函数,而这两个函数又再分别调用2次。我们不难发现调用的次数呈指数增长。读者可以尝试用这两个代码分别从第35个月开始依次增加一个月,看运行的速度以及结果。