递归

函数调用原理

  在理解递归之前,不得不先了解一下函数调用的工作原理。

 

  在程序运行期间调用一个函数的时候,在运行被调用函数之前,需要完成三项任务。

  1.将所有的实参、返回地址等信息传递给被调用函数。

  2.为被调用函数的局部变量分配存储区。

  3.将控制转移到被调用函数的入口。

 

  函数执行完返回之前,应该完成下列三项任务。

  1.保存当前函数的最终计算结果。

  2.释放被调函数的数据区。

  3.根据返回地址将控制转移到上层,也就是调用方。

 

  在函数之中调用函数也是同样的道理,如A函数中调用B函数,B函数中调用C函数。

  那么C函数执行完之后程序的控制会回到B函数中调用C的地方继续往下执行,B执行完后会回到A中继续执行,整个流程的运行规则是后调用先返回。

 

  能够实现后进先出的数据结构是栈,此时的内存管理实行的就是 “栈式管理” ,这是栈的一个典型应用。

递归函数

  一个直接调用自己或间接调用自己的函数,称为递归函数。

 

  递归必须要满足两个元素,一是递归出口,二是递归表达式。

  递归出口即为返回条件,没有递归出口的递归那是死循环。 递归表达式即是直接或者间接调用自身的行为,如果你不调用自身,那么这个算法也不能够叫做递归。

 

  所谓间接调用自己,意思就是你调用别的函数,别的函数里面又在调用你。

 

  我们设计递归函数可以根据几种情况考虑:

  第一种情况,问题的定义是递归的,比如数学中的阶乘,像这样子的问题我们直接就可以写出递归函数。

  第二种情况,有些数据结构比如二叉树广义表,由于结构本身存在递归特性,则可以使用用递归函数来描述。

  第三种情况,问题本身没有明显的递归特征,但使用递归求解更简单,比如Hanoi塔问题。

  前两种情况比较直观,只要我们一遇到,就会很直接的想到用递归来处理,第三种情况则需要更多的经验来判断是否应该使用递归。

 

阶乘函数

  一个正整数的阶乘是所有小于或等于该数的正整数的乘积,0的阶乘为1。

  比如5的阶乘 = 5 * 4 * 3 * 2 * 1 = 120;

 

递归代码:

function fact($n){
    if(0 == $n){
        return 1;//递归出口
    }
    $temp = $n * fact($n-1);//递归表达式
    return $temp;
}

$result = fact(5);

 

如果描绘出递归工作栈的变化情况会非常的直观:

 

Hanoi塔问题

  Hanoi塔 也叫做汉诺塔。

 

题目:

如图所示,假设有三个分别命名为X、Y和Z的塔座,在X上插的有N个直径大小各不相同,并从大到小依次堆叠的圆盘,现要求将X塔上的所有盘子移动到Z塔上,并且堆叠的顺序保持不变。

圆盘移动时必须遵循以下规则:

1、每次只能移动一个圆盘
2、圆盘可以放到X,Y,Z中任一塔座上
3、任何时候不能将一个较大的圆盘压在较小的圆盘之上

 

  在我们不知道递归之前,按照常规做法,我们都会使用递推的方式,比如1个圆盘的情况怎么解决,2个圆盘的情况怎么解决,3个圆盘的情况怎么解决,将多种情况递推出来,然后找规律。

 

  我们将盘子的个数称为N,盘子大小即为n的大小。

  当N=1时,直接将1号盘子搬到Z号塔就结束了。

  当N=2时,将1号盘子搬到Y上,再将2号盘子搬到Z上,再将1号盘子从Y搬到Z上。

  当N=3时,将1号盘子搬到Z上,2号盘子搬到Y上 ,1号盘子从Z搬到Y上,3号盘子搬到Z上,1号盘子搬到X上,2号盘子从Y搬到Z上,1号盘子从X搬到Z上。

  .........

  我们发现,根据N的不同,盘子的搬法是不一样的,使用递推的方法,很难找到规律。

 

  据说国外有个皇帝曾经为了解这个题,叫手下的人实际操作搬盘子,然而搬了N多天也没个结果。 因为他们的盘子有点多,用了64个盘子。

  后来得出的结论是,移动盘子的次数是2的N次方-1,就算1秒钟移动一次,要搬完这64个盘子,需要5845.54亿年,那时候地球早已经毁灭了。

 

  既然常规思路走不通,我们就换个思路考虑问题。

 

  我们可以把问题分解成下面三步

  第一步,我们想办法把N-1个盘子从X搬到Y上。

  第二步,把X上的最后一个盘子搬到Z上。

  第三步,把Y上的N-1个盘子搬到Z上。

  这三步实现的话,那么整个事情也就完成了。

 

  不管你的N有多大,我们只把它拆分为N-1和1两个规模,当剩下的盘子等于1时,那么我们就直接搬到Z上就行了。

 

  那么我们面临的最大的问题,就是第一步和第三步中的搬动N-1个盘子,要怎么搬?

  此时此刻,我们面临的这个问题,和最开始的问题完全一样。但问题规模不一样了,原来是N个盘子的问题,现在我们把它变成了N-1个盘子的问题。

 

  同样,N-1的问题,不就等于N-2的问题吗?不就等于N-3的问题吗?

 

  那么我们进一步分析

  第一步中,把N-1个盘子从X搬到Y上,此时起始位置是X,目标位置是Y,中间要借助Z。

  第二步就简单了,直接把X上的盘子移动到Z上。

  第三步中,再把Y上的N-1个盘子搬到Z上,此时起始位置是Y,目标位置是Z,中间要借助X。

 

  他们的每一次移动,都要遵守这三步的规则,做法都是一样,唯有传递的不同。

  有了上面这些思考,我们再来看代码,配合着注释,我想更容易帮助理解。

 

/**
 * @param $n 盘子编号
 * @param $x 起始位置
 * @param $y 辅助塔
 * @param $z 目标位置
 */
function hanoi($n,$x,$y,$z){
    if($n==1){
        move($x,1,$z); //将最后一个圆盘从x搬到z上
    }else{
        hanoi($n-1,$x,$z,$y);//将x上N-1个盘子从x搬到y上  z是辅助塔
        move($x,$n,$z);//将编号为N的盘子从x搬到z上
        hanoi($n-1,$y,$x,$z);//将y上N-1个盘子从y搬到z上  x是辅助塔
    }
}

function move($x,$n,$z){
    echo '移动第'.$n.'号盘子 从'.$x.'-->'.$z.'<br>';
}

hanoi(5,'x','y','z');

 

  友情提示:不要去想具体的移动,你一但去想,就陷入了递推的思维中,那样你会疯掉的。对于递归的理解最重要的是放弃你在脑海中推导细节的冲动,汉诺塔问题永远只有两层,就是N-1和1,当N>1时他们以同样的方式移动。

 

递归思维

  在讨论Hanoi塔问题的时候我们面临了两种思维的选择,递推与递归,这里进行比较一下,这两种思维到底有什么区别。

  以登山为例,如华山9000级台阶,如何登上这9000级台阶?

 

  递推思维:我先抬脚登上1级台阶,就还剩8999级台阶,然后再登1级台阶,就还剩8998级台阶,登一级台阶又不费力,只需重复这个动作,最终即可登顶。

  递归思维:如果我现在在第8999级台阶上,我只需要抬脚一步就可以登顶。那么我怎样才能到第8999级台阶上呢? 如果我现在在第8998级台阶上,我只需要抬脚一步就可以到达8999级台阶,以此类推。

 

  虽然说,最终实践起来都是一步一步往上爬,没什么区别,但两种思维讨论问题的出发点不一样,递推从易到难,递归从难到易,编写出来的算法程序是两个派别。

 

posted @ 2019-08-02 09:56  不该相遇在秋天  阅读(734)  评论(0编辑  收藏  举报