什么是递归?
之前说到,递归是一种将大问题分解为小问题的解决方案。一般来说,递归被称为函数自身的调用。这么说可能听起来很奇怪,事实上在递归中,函数确实必须调用自己。
一个栗子
例如在数学中,我们都知道“阶乘”的概念。例如5的阶乘就是5*4*3*2*1
。
- 5!= 5 * 4!
- 4!= 4 * 3!
- 3!= 3 * 2!
- 2!= 2 * 1!
- 1!= 1 * 0!
- 0!= 1
我们可以总结出求n的阶乘的规律,即 n! = n * (n -1) !
这就体现了递归。你可以从中发现,我们把求5的阶乘一步一步转化成了另外一个个的小问题。
递归算法的特性
- 每一个递归调用都必须基于一个小的子问题。例如5的阶乘就是5乘4的阶乘。
- 递归必须有一个Base case。例如阶乘的Base case就是0,当条件是0的时候,就停止递归。
- 递归中避免循环调用,否则最后计算机会显示栈溢出的错误。
function factorial(int $n): int
{
if ($n = 0) {
return 1;
}
return $n * factorial($n - 1);
}
看上面的代码,我们可以看到对于阶乘问题的解决方案我们有一个基础的条件就是当n为0的时候,我们返回1。如果不符合这个条件,我们返回n
乘 factorial(n)
,这符合递归特性的第一条和第三条。我们避免了循环调用,因为我们把每一次的递归调用都分解成了大问题的一个小的子问题。上面的算法思想可以表达成:
递归Vs迭代
上面的递归代码我们同样可以使用迭代的方法实现
function factorial(int $n): int
{
$result = 1;
for ($i = $n; $i > 0; $i--) {
$result*= $n;
}
return $result;
}
如果一个问题可以很容易的使用迭代来解决,我们为何要使用递归?
递归是用来处理更加复杂的问题的,不是所有的问题都可以简单的使用迭代来解决的。递归使用函数调用来管理调用栈,所以相比于迭代递归会使用更多和时间以及内存。此外,在迭代中,我们每一步都会有一个结果,但是在递归中我们必须等到base case执行结束才会有任何结果。看上面的例子,我们发现在递归算法中我们没有任何变量或者声明来保存结果,而在迭代算法中,我们每一次都用$result来保存了返回结果。
斐波那契数列
在数学中,斐波那契数列是一个特殊的整数数列,数列中的每一个数的是由另外两个数求和产生的。规则如下:
function fibonacci($n)
{
if ($n == 0) {
return 0;
}
if ($n == 1) {
return 1;
}
return fibonacci($n - 1) + fibonacci($ - 2);
}
最大公因数
另外一个使用递归算法的常见问题是求两个数的最大公因数。
function gcd(int $a, int $b)
{
if ($b == 0) {
return $a;
}
return gcd($b, $a % $b);
}
递归类型
- 线性递归
在每一次递归调用中,函数只调用自己一次,这就叫做线性递归。
- 二分递归
在二分递归中,每一次递归调用函数调用自己两次。求解斐波那契数列的算法就是二分递归,除此之外还有二分查找、分治算法、归并排序等也使用了二分递归。
- 尾递归
当一个递归返回的时候没有等待的操作的时候就称为尾递归。斐波那契算法中,返回值需要乘以前一个递归的返回值,因此他不是尾递归,而求解最大公因式的算法是尾递归。尾递归是线性递归的一种形式。
- 相互递归
例如在每一次递归调用中 有 A() 调用 B(), B() 调用 A() ,这样的递归就叫做相互递归。
- 嵌套递归
当一个递归函数把自己作为一个参数进行递归调用时,就叫做嵌套递归。一个常见的栗子就是阿克曼函数,看下面的表达。
看最后一行的,可以看到第二个参数就是递归函数自己。
下一节
下一篇内容会使用递归解决一些实际开发中会遇到的问题,例如构建N级分类、构建嵌套评论、目录文件的遍历等等。
更多内容
PHP基础数据结构专题系列目录地址:地址 主要使用PHP语法总结基础的数据结构和算法。还有我们日常PHP开发中容易忽略的基础知识和现代PHP开发中关于规范、部署、优化的一些实战性建议,同时还有对Javascript语言特点的深入研究。