递归学习
// 查询数组最大值 $r为count($arr)-1
1 2 3 4 5 6 7 8 9 10 11 12 13 | function findMax( $arr , $r , $l = 0) { if ( $l == $r ) { return $arr [ $l ]; } else { $a = $arr [ $l ]; $b = findMax( $arr , $r , $l +1); if ( $a > $b ) { return $a ; } else { return $b ; } } } |
// 数组从小到大排序 $r为count($arr)-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function sortArr(& $arr , $r , $l = 0) { if ( $r == $l ) { } else { for ( $i = $l +1; $i <= $r ; $i ++) { $a = $arr [ $i ]; $b = $arr [ $l ]; if ( $a < $b ) { $arr [ $l ] = $a ; $arr [ $i ] = $b ; } } sortArr( $arr , $r , $l +1); } } |
// 费波纳切数列 1 1 2 3 5 8 13
1 2 3 4 5 6 7 | function getFB( $n ) { if ( $n < 3) { return 1; } else { return getFB( $n -1) + getFB( $n -2); } } |
/* 汉诺塔
*有三根柱子,原始装满大小不一的盘子的柱子我们称为A,还有两根空的柱子,我们分别称为B和C(任选)最终的目的就是将A柱子的盘子全部移到C柱子中移动的时候有个规则:一次只能移动一个盘子,小的盘子不能在大的盘子上面(反过来:大的盘子不能在小的盘子上面)
*/
1 2 3 4 5 6 7 8 9 | function getHNT( $n , $start = 'A' , $transfer = 'B' , $target = 'C' ) { if ( $n == 1) { echo $start . '-->' . $target , '<br />' ; } else { getHNT( $n -1, $start , $target , $transfer ); echo $start . '-->' . $target . '<br />' ; getHNT( $n -1, $transfer , $start , $target ); } } |
每次执行完s指令,都会有一层递归调用终止,直到返回main函数。事实上,如果在递归调用初期查看调用栈,则会发现每次递归调用都会多一个栈帧——和普通的函数调用并没有什么不同。确实如此。由于使用了调用栈,C语言自然支持了递归。在C语言的函数中,调用自己和调用其它函数并没有什么本质区别,都是建立新栈帧,传递参数并修改当前代码行。在函数体执行完毕后删除栈帧,处理返回值并修改当前代码行。
我首先在知乎上发现了下面两个例子,对比了递归和循环。
递归:你打开面前这扇门,看到屋里面还有一扇门(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开,..., 若干次之后,你打开面前一扇门,发现只有一间屋子,没有门了。你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这钥匙开了几扇门。
循环:你打开面前这扇门,看到屋里面还有一扇门,(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,(前面门如果一样,这门也是一样,第二扇门如果相比第一扇门变小了,这扇门也比第二扇门变小了(动静如一,要么没有变化,要么同样的变化)),你继续打开这扇门,...,一直这样走下去。 入口处的人始终等不到你回去告诉他答案。
该用户这么总结到:递归就是有去(递去)有回(归来)。
具体来说,为什么可以”有去“?
这要求递归的问题需要是可以用同样的解题思路来回答除了规模大小不同其他完全一样的问题。
为什么可以”有回“?
这要求这些问题不断从大到小,从近及远的过程中,会有一个终点,一个临界点,一个baseline,一个你到了那个点就不用再往更小,更远的地方走下去的点,然后从那个点开始,原路返回到原点。
上面的解释几乎回答了我已久的疑问:为什么我老是有递归没有真的在解决问题的感觉?
因为递是描述问题,归是解决问题。而我的大脑容易被递占据,只往远方去了,连尽头都没走到,何谈回的来。
《漫谈递归:递归的思想》这篇文章将递归思想归纳为:
递归的基本思想是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。
需注意的是,规模大转化为规模小是核心思想,但递归并非是只做这步转化,而是把规模大的问题分解为规模小的子问题和可以在子问题解决的基础上剩余的可以自行解决的部分。而后者就是归的精髓所在,是在实际解决问题的过程。
我试图把我理解到递归思想用递归用程序表达出来,确定了三个要素:递 + 结束条件 + 归。
function recursion(大规模)
{
if
(end_condition){
end;
}
else
{
//先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
recursion(小规模);
//go;
solve;
//back;
}
}
function recursion(大规模)
{
if
(end_condition){
end;
}
else
{
//在将问题转换为子问题描述的每一步,都解决该步中剩余部分的问题。
solve;
//back;
recursion(小规模);
//go;
}
}
总结到这里,我突然发现递归是为了最能表达这种思想,所以用“递归”这个词,其实递归可以是“有去有回”,也可以是“有去无回”。但其根本是“由大往小地去,由近及远地去”。“递”是必需,“归”并非必需,依赖于要解决的问题,有的需要去的路上解决,有的需要回来的路上解决。有递无归的递归其实就是我们很容易理解的一种分治思想。
其实理解递归可能没有“归”,只有去(分治)的情况后,我们应该想到递归也许可以既不需要在“去”的路上解决问题,也不需要在“归”的路上解决问题,只需在路的尽头解决问题,即在满足停止条件时解决问题。递归的分治思想不一定是要把问题规模递归到最小,还可以是将问题递归穷举其所有的情形,这时通常递归的表达力体现在将无法书写的嵌套循环(不确定数量的嵌套循环)通过递归表达出来。
将这种递归情形用递归程序描述如下:
recursion()
{
if
(end_condition){
solve;
}
else
{
//在将问题转换为子问题描述的每一步,都解决该步中剩余部分的问题。
for
(){
recursion();
//go
}
}
}
由这个例子,可以发现这种递归对递归函数参数出现了设计要求,即便递归到尽头,组合的字符串规模(长度)也没有变小,规模变小的是递归函数的一个参数。可见,这种变化似乎一下将递归的灵活性大大地扩展了,所谓的大规模转换为小规模需要有一个更为广义的理解了。
对递归的理解就暂时到这里了,可以看出文章中提到关于“打开一扇门”的递归例子来解释递归并不准确,例子只描述了递归的一种情况。而“递归就是有去(递去)有回(归来)”的论断同样不够准确。要为只读了文章前半部分的读者惋惜了。
我也给出自己对递归思想的总结吧:
递归的基本思想是广义地把规模大的问题转化为规模小的相似的子问题或者相似的子问题集合来解决。广义针对规模的,规模的缩小具体可以是指递归函数的参数,也可以是其参数之一。相似是指解决大问题的方法和解决小问题的方法往往是同一个方法,还可以是指解决子问题集的各子问题的方法是同一个方法。解决大问题的方法可以是由解决次规模问题的方法和解决剩余部分的方法组成,也可以是由一系列解决次规模问题的方法组成。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了