• 请不要在回答技术问题时复制粘贴 AI 生成的内容
justfly
V2EX  ›  程序员

为什么我不习惯用递归,这方面我应该怎么加强?

  •  
  •   justfly · May 8, 2013 via iPhone · 17747 views
    This topic created in 4928 days ago, the information mentioned may be changed or developed.
    这个问题很苦恼。

    比如说我想计算一个数的阶乘 n! 我首先想到的是用for循环一个一个的乘,根本想不到递归的方法。

    当然这只是一个例子,当初学数据结构,二叉树遍历代码很简单,但因为是递归的,就觉得理解不透的感觉,汉诺塔那个递归算法也是记不住。太苦恼了

    虽然所有递归理论来说都可以用普通方法代替,但是递归代码清晰易懂的优势也很大,所以请不要说难理解就别深究这样的话。

    请各位v2exer解惑,感谢!
    81 replies  •  1970-01-01 08:00:00 +08:00
    messense
    messense
    May 8, 2013
    1
    人理解迭代,神理解递归。。。实在想不明白就这么安慰自己好了。
    leiz
    leiz
    May 8, 2013
    2
    递归说白了就是数学归纳法
    chemzqm
    chemzqm
    May 8, 2013
    3
    不用递归会对你写出好代码有多大影响?
    darasion
    darasion
    May 8, 2013
    4
    @leiz 说的对。就当是用数学归纳法证明一道数学题。
    holmesabc
    holmesabc
    May 8, 2013
    5
    其实把入与出的条件想清楚,就好理解了
    iloahz
    iloahz
    May 8, 2013
    6
    我觉得自己跟着递归的程序跑一跑挺好的,就是在大脑里一条一条的执行语句,比如hanoi
    Cadina
    Cadina
    May 8, 2013❤️1
    7
    不要尝试理解递归,理解数学归纳法或循环不变式
    YUCOAT
    YUCOAT
    May 8, 2013
    8
    最近我在研究红黑树,感觉红黑树实现起来好复杂。。
    后来想想其实不算复杂,之所以觉的复杂,无非是因为自己没有理解透。所以啊,想办法先理解透了再说吧。
    detailyang
    detailyang
    May 8, 2013
    9
    要理解递归,首先要理解递归 = =,我也被搞死,二叉树,深度优先 = =
    solesschong
    solesschong
    May 8, 2013
    10
    做纯程序员才用到递归。实在不喜欢递归可以去做前台,做应用。
    Golevka
    Golevka
    May 8, 2013
    11
    Go functional yourself, Webvolutionaries!
    wodemyworld
    wodemyworld
    May 8, 2013❤️1
    12
    盗梦空间就是个递归系统,最后很可能回到现实了还return呢,或者在某一层梦境中丢了陀螺,结果出不来了
    如果不是十分自信,就别用递归了,耗栈区资源,切换上下文频繁,也没多大收益,现在基本没人用专门的递归机器(硬件)了,所以能转换成for循环的就别用这货了。。。。
    alexrezit
    alexrezit
    May 8, 2013
    13
    我觉得递归理解起来更简单啊.
    用循环总是搞错次数, 用递归就从来不会.
    liwei
    liwei
    May 8, 2013
    14
    xatest
    xatest
    May 8, 2013❤️3
    15
    从机器执行程序的角度来说迭代的效率肯定高于递归,因为递归在执行时需要函数嵌套好多层,对函数栈的消耗相当大,看上去就如同一段缩进了很多层的代码。所以LZ能优先用迭代而不是递归的话,反而是一个优势啊,我往往就想不到,LZ没什么好苦恼的。
    但是从人理解的容易程度上讲,递归更容易理解吧。试想一个典型的递归结构,就是文件系统里的目录,每个目录里可能还有目录。如果要写一个函数,列出文件系统上所有目录和文件,最容易想到的算法是什么?首先写一个函数,列出当前层的所有目录和文件(类似ls命令),如果列出的是目录,再调用自身,不就解决了。
    glume
    glume
    May 8, 2013
    16
    碰见类似的问题。曾写帖子“回复”代码,一层回复一层,如果不停的回复前面的回复,嵌套起来压力很大。
    youlil
    youlil
    May 8, 2013
    17
    @leiz 递归和数学归纳法是正反的区别
    swulling
    swulling
    May 8, 2013
    18
    生产环境老板要写个阶乘,谁要写个递归版本的,直接被开掉
    Golevka
    Golevka
    May 8, 2013
    19
    @swulling
    sub factorial {
    reduce {$a * $b} 1, 1 .. $_[0];
    }
    ML与Haskell里也有fold类似物, 但是fold也是递归定义的.
    davepkxxx
    davepkxxx
    May 8, 2013
    20
    递归我能不用就不用。
    plan9
    plan9
    May 8, 2013
    21
    @davepkxxx 记得某本书里写过:我的程序员要是用递归去计算阶乘,我宁愿换人。
    nsa
    nsa
    May 8, 2013
    22
    @plan9 代码大全。。。
    C0VN
    C0VN
    May 8, 2013❤️1
    23
    递归的规则很简单
    1,需要调用本身
    2,有个结束的条件


    把一个大问题转化为同样的小问题,递归不就是就是不停的乘以比他大1的数吗。 n*f(n+1)

    为了好判断终止条件,我们换成换成 n*f(n-1)

    在函数内部设定个终止条件

    def f(n):
    ____if n == 1:
    ________return 1

    这个函数内部的n显然不再是外面带进去的那个n了,每个函数中都有一个不一样的n。

    def f(n):
    ____if n == 1:
    ________return 1
    ____else:
    ________return n*f(n-1)

    再了解递归可以去看看点稍微复杂点的例子,像二分法求平方根,快速排序。
    nsa
    nsa
    May 9, 2013
    24
    孩子买书去学吧,推荐 莫绍揆的《递归论》
    oyasmi
    oyasmi
    May 9, 2013
    25
    @liwei 嗯,同样推荐SICP,很强调递归的说,尤其习题要做一下。
    lululau
    lululau
    May 9, 2013
    26
    @xatest 同意,我也觉得递归是对一个问题的更容易的解决方式,同样也是更没运行效率的一种解决方式
    sectic
    sectic
    May 9, 2013 via iPhone
    27
    递归可以写阶乘,不慢。
    迭代就是加了一个计数参数的尾递归。
    递归有些时候不用就搞不出来。
    sectic
    sectic
    May 9, 2013 via iPhone
    28
    吐个血槽,scheme 读输入都是拿递归
    monkeylyf
    monkeylyf
    May 9, 2013
    29
    写一点functional programming的东西 scala, haskell 不想递归也得递归
    Ricepig
    Ricepig
    May 9, 2013
    31
    可以学学分形几何嘛,娃哈哈哈哈,自相似
    csslayer
    csslayer
    May 9, 2013
    32
    写 functional programming,把循环想成尾递归
    davepkxxx
    davepkxxx
    May 9, 2013
    33
    我记得我之前学习Scala,因为其不支持break而苦恼,结果Google Groups上的人推荐我用递归代替循环……
    williamx
    williamx
    May 9, 2013
    34
    说明你是一个比较勤快的人---->当你变懒了之后,你就会想用递归等等----当然,优秀的程序员都是“懒”的。
    fooCoder
    fooCoder
    May 9, 2013
    35
    @solesschong 什么是纯的程序员。。。
    solesschong
    solesschong
    May 9, 2013 via iPad
    36
    @fooCoder 就是真正工作的时候很可能用不到。除非写很理论的东西
    tyzc
    tyzc
    May 9, 2013
    37
    @xatest 赞同。递归简单理解就是函数调用函数,不过调用的函数是自己而已。
    fooCoder
    fooCoder
    May 9, 2013
    38
    @solesschong 也就是说你的意思是“纯的程序员”,就是搞理论的?
    celadevra
    celadevra
    May 9, 2013❤️1
    39
    可以跟一遍这个课程:https://class.coursera.org/proglang-2012-001/class/index
    虽然只是个大三大四的课,其中大部分 SML 和 Racket 写的作业都要用到递归,做完一遍就差不多养成习惯了。
    primer
    primer
    May 9, 2013❤️1
    40
    这个我觉得主要靠练习,我刚开始接触递归的时候,也是像楼主一样搞不懂。 后来多想想,多练习,也发现没什么难的。
    楼主可以从栈这方面理解,递归说白了就是不断的压栈呀,出栈...
    skywalker
    skywalker
    May 9, 2013
    41
    用一段Haskell就习惯了,其实更多是一种思维训练,如果找出问题的规律,将其分解为更小规模的问题.
    zhujinliang
    zhujinliang
    May 9, 2013
    42
    业余玩单片机的表示没法用递归,内存按字节数,一搞递归要吃多少内存啊
    46fo
    46fo
    May 9, 2013
    43
    递归 很好么??? 小心栈溢出
    zhicheng
    zhicheng
    May 9, 2013
    44
    一句话,学并且用 Clojure 。在类C的语言里,不要使用递归。
    justfly
    justfly
    OP
    May 9, 2013
    45
    @leiz 经你一说觉得确实是这样,我再温习一遍数学归纳法
    justfly
    justfly
    OP
    May 9, 2013
    46
    @xavierskip

    恩,以后递归的问题从这两点出发去考虑
    oldcai
    oldcai
    PRO
    May 9, 2013
    47
    @xdeng 同忧心。
    justfly
    justfly
    OP
    May 9, 2013
    48
    @zhujinliang 当然单片机这种寸土寸金的地方就不考虑这些啦
    skywalker
    skywalker
    May 9, 2013
    49
    @xdeng 用尾递归
    Mutoo
    Mutoo
    May 9, 2013❤️1
    50
    计算机上的递归实际上就是 栈+循环 而已。
    skywalker
    skywalker
    May 9, 2013
    52
    @onesuper 牛!太好了
    Cadina
    Cadina
    May 9, 2013❤️1
    53
    @zhujinliang 说递归有效率问题的,可以看下尾递归优化
    solesschong
    solesschong
    May 9, 2013 via iPad
    54
    @fooCoder 是。我不和你说了,再说没钱了。刚注册
    yexiang841
    yexiang841
    May 9, 2013
    55
    生产环境下谁用递归开除谁,无二话。
    zhujinliang
    zhujinliang
    May 9, 2013
    56
    @Cadina 谢谢,明天早上研究一下
    kylefeng
    kylefeng
    May 9, 2013
    57
    去看 《The Little Schemer》 吧。
    diligence24
    diligence24
    May 9, 2013
    58
    我觉得写递归要想清楚什么时候return。具体的问题你可以debug,不过切记测试数据要简单却不失代表性。不然不会单步到找不着北的。。。
    seeker
    seeker
    May 9, 2013
    59
    学一门函数式语言。lisp,ML,haskell自动就会了
    tioover
    tioover
    May 10, 2013 via Android
    60
    我不太习惯用迭代,这方面我该做什么加强。
    Idiosyncratic
    Idiosyncratic
    May 10, 2013
    61
    @Cadina 尾递归只能优化尾递归,又不能优化各种递归,如果是递归函数里调用两次递归(深搜二叉树),这样还是没法优化啊。。, 所以递归递归效率的确不高
    Cadina
    Cadina
    May 10, 2013
    62
    @Idiosyncratic 但是树型递归这类问题,递归产生的cost是必须的,即使不用递归也需要手动维护栈或者类似的数据结构,和递归的耗费是一样的,这就不是递归本身的性能问题了,你没有其他的选择。
    所以你说的效率不高其实是算法的问题,而不是递归的问题。
    引入尾递归优化的价值在于,用更抽象的代码解决问题,并且没有额外的性能消耗。
    Idiosyncratic
    Idiosyncratic
    May 10, 2013
    63
    @Cadina 尾递归只是一个编译优化而已,没你说的那么神奇吧,还是那句话,尾递归只能优化在递归函数最后调用递归的问题,而且做法就是把这样的递归改成与for循环类似的样式;
    递归的简洁、抽象是必须的,但是在实际系统里它的效率问题也很明显。你认为递归和手动维护这样的耗费是一样的,这个我觉的不准确,递归的性能消耗有这么几个方面:
    1、它每次都需要把所有的局部变量压栈,不管这个局部变量还要不要用;
    2、它是一个函数一个函数为单位进行调用的,调用函数是有消耗;
    以上两个方面在迭代次数不大的时候其实不是啥问题,但是次数一多就会成为问题了;
    而递归需要的消耗,在for循环一类的迭代里完全可以避免。
    就拿深搜来说,一般用for循环迭代之前,都会先开一个容器(数组,字典都行),把所有节点的初始状态保存进去,然后每遍历一个节点修改容器里相应的元素内容,很多在递归里需要的消耗(压栈,出栈,跳转等)在这就没有了;

    尾递归优化当然是很好的东东,但是目前应该还没有可以把所有递归都优化的很好的法子;所以我感觉递归的效率还是有些低。

    当然,递归最大的问题还是它很容易把栈给挤爆。。。
    Golevka
    Golevka
    May 10, 2013
    64
    @Idiosyncratic "一般用for循环迭代之前,都会先开一个容器(数组,字典都行),把所有节点的初始状态保存进去,然后每遍历一个节点修改容器里相应的元素内容"

    这做法整个一杯具啊. 比如考虑遍历一个N个节点的二叉树, 用DFS递归遍历的渐进空间复杂度只有O(logN), 开一个容器然后迭代的空间复杂度总是O(n). 如果要DFS的二叉树是平衡的那么递归的空间开销则可以直接忽略了. 遍历一颗1T节点的平衡二叉树所需要的堆栈桢也不会超过100个单位. 你有1T内存还拿不出100个函数调用的栈桢么?

    递归的空间消耗是由递归的深度和tail call共同决定的. 在能保证递归深度的渐进增长率是可接受的情况下直接递归也不会有什么问题, 尤其是递归代码显然更简洁且易维护的情况下.
    unionx
    unionx
    May 10, 2013
    65
    递归我能不用就不用+1
    Golevka
    Golevka
    May 10, 2013
    66
    @unionx 发现野生帝老师
    Idiosyncratic
    Idiosyncratic
    May 10, 2013
    67
    @Golevka 大哥,我觉得你混淆了好多情况啊。。:
    1、时间复杂度:一个算法的空间复杂度(Space Complexity)S(n)定义为该算法所耗费的存储空间,是对一个算法在运行过程中临时占用存储空间大小的量度; 递归算法在结束全所有节点都不得释放,O(logN)能存的下O(N)级的内容? 不论是啥样的dfs算法,需要的空间起码是O(n)的,因为是遍历n个节点,起码记录节点遍历没有的标记符就要有N个; O(logN)那是完全二叉树的高度,是完全二叉树的递归层数,可以说是一个特例而已,也只能说是和空间相关。

    2、dfs的遍历方式是给定的,只要照着dfs做,除非故意找茬(故意开个超大内存啥的),算法的空间时间复杂度都是一样;

    3、栈空间和内存空间不是一回事,vc6的C语言编译器默认的栈空间只有1M(其他环境相差不大,反正默认的都不大),不然大家怎么会天天说递归容易爆栈?想要扩大栈空间只有在程序运行前在IDE或是命令行里指定,就是说程序本身决定不了,这样的递归,除非你每次运行前都先估摸这一下要用多少栈空间,不然应付各种规模数据的时候总会有爆掉的一天;相比来说,可以在内村里手动开辟的法子显然更好;

    4、有一个很重要的问题,我再重复一下,dfs的空间、时间复杂度,在算法上是一样的,不是说递归层数变少,空间就变小了;层数变少,只是要存的局部变量就是变少了,但是用其他方法实现的时候完全可以不存储那些局部变量:
    int a(){
    int b;
    if xx:
    return;
    a();
    }
    b要存下;

    while(!xxx){
    int b
    &(&(
    }
    b不用存;

    因此,在理论上时间,空间一致的情况下,递归还有各种实现上的开销,所以效率低;

    PS:这句话:“都会先开一个容器(数组,字典都行),把所有节点的初始状态保存进去”,存的就是二叉树好吧,二叉树经常这么存储,然后在每个节点元素里加上个种信息。
    本来还想在写点,但是看着已经写好多了,在写下去就太烦了。。,主要是觉得你没搞懂。。。还说我的法子不对。。有些不爽。。
    ccinls
    ccinls
    May 10, 2013
    68
    递归除了让你感受到算法的奥妙的之外,基本毫无用处。况且,能不用递归,尽量不要用递归。不过这个思维过程比较有趣~
    PS:我算是半吊子玩算法的。
    Idiosyncratic
    Idiosyncratic
    May 10, 2013
    69
    @Golevka 呃,一时手快,刚刚的回复打错了几个字

    “1、时间复杂度” => 想打的是“1、空间复杂度”

    第5行“可以说是一个特例而已,也只能说是和空间相关。” => 其实我想说的是,完全二叉树是性质很好一个种树。。
    myrual
    myrual
    May 10, 2013 via iPhone
    70
    反复练习
    Golevka
    Golevka
    May 10, 2013
    71
    @Idiosyncratic
    0. 呀, 如果度量算法空间复杂度还要算上数据本身占用的空间的话, 你能解释一下quicksort的O(logN)的空间复杂度是怎么得来的么?
    1. 我前面提到的1T单位的空间是[存储]二叉树需要的空间, 而非执行递归调用所需的[栈空间];
    2. 虽然某些方法能获得更好的渐进复杂度; 但这种[更好]有时只是"galactic improvments". 你认为O(logN)和O(1)有多大的差距, 在你的计算机所能承受的数据范围内?
    Idiosyncratic
    Idiosyncratic
    May 10, 2013
    72
    @Golevka
    0. 空间复杂度和渐进空间复杂度嘛,你本来说渐进空间复杂度,但是却说存储了节点后的算法(我原来给的算法)的渐进空间复杂度是O(N)。而这O(N)是dfs的空间复杂度。。。
    1、至于一颗1T的树。。,具体的总有trick的法子可以不用一口读这么多的,但是递归同时要存储占用更多的栈空间;具体的我没啥数据,不过我以前多一个500多M的文件夹内txt文件(随机生成的)进行单词词频统计,用递归的法子把栈挤爆过;
    2、 这个问题。。,和递归效率有关系吗,咱们没讨论这个吧。。
    Golevka
    Golevka
    May 10, 2013
    73
    @Idiosyncratic
    0. http://en.wikipedia.org/wiki/Depth-first_search
    Hint: 右侧的示意图下面有个大大的O(|V|), 并且Properties中也明确说"space complexity of this variant of DFS is only proportional to the depth limit"

    1. 我之所以提到1T balanced binary tree并非强调把数据读入内存有多困难或者应该用怎样的trick分块处理, 而是强调O(logN)的增长速度很慢以至于N[非常大]时也很足够小, 在实际应用场合也是可以接受的. (一般认为log(N) in real world <= 64);

    2. 同#1
    volCANo
    volCANo
    May 10, 2013
    74
    即使所有的递归能弄成尾递归,调用函数的开销也会使速度变慢。当然,如果调用函数的开销只占所有开销很小一部分的话,还是用尾递归较好,用尾递归的代码更好看,更容易理解。
    Idiosyncratic
    Idiosyncratic
    May 10, 2013
    75
    @Golevka
    0、我真的被你搞糊涂了,空间复杂度是O(N)没错啊,我都说了几遍了,(V是指节点个数,和我们的N是一个意思,E是边数,无环图下E=V-1),空间复杂度和渐进空间复杂度不同。。fine,你自己琢磨一下我们帖子的对答吧,;至于wiki的话,不能忽视上下文吧:“In this case, the time is still linear in the number of expanded vertices and edges (although this number is not the same as the size of the entire graph because some vertices may be searched more than once and others not at all) but the space complexity of this variant of DFS is only proportional to the depth limit”
    我就不具体翻译了,大意是 "时间依然是 节点数和边数和的 倍数;但是这个dfs变种算法的空间复杂度是受深度限制的"; 这个算法是个变种,不记录节点是否遍历,和我提的那个不同;

    1、 这个,空间复杂度都是N,渐进复杂度,没错递归是O(logN),但是非递归的渐进复杂度。。。,我就不仔细解释了,一次for循环只抓一个节点。。。;
    2、同#1
    Golevka
    Golevka
    May 10, 2013
    76
    @Idiosyncratic
    0. 晕... 抱歉我会错意了. 我本来想说因为遍历的是tree所以可以不维护visited set的 = =
    1. 前面说好的要[把所有节点的初始状态保存进去]呢, 难道这一步不需要O(|V|)的空间么
    Golevka
    Golevka
    May 10, 2013
    77
    @Idiosyncratic 好吧我知道了, 乃刚才的讨论都是针对任意图的DFS, 我只想到遍历树了
    wy315700
    wy315700
    May 10, 2013
    78
    能用循环尽量用循环 递归是个坑 不好调试也容易崩溃,尤其深度过大的时候容易爆栈

    图灵证明过 循环和递归是等价的
    DFS可以无缝转换成BFS
    Idiosyncratic
    Idiosyncratic
    May 10, 2013
    79
    @Golevka 奥,这样啊。。,好像是哦。。,= =
    Cadina
    Cadina
    May 10, 2013
    80
    @volCANo 尾递归的优化就是把函数调用优化成循环。。。
    volCANo
    volCANo
    May 12, 2013
    81
    @Cadina 不是吧!?,尾递归的优化是去除爆栈的可能性,还是会产生函数调用的消耗的。
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   1498 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 1070ms · UTC 19:13 · PVG 03:13 · LAX 11:13 · JFK 14:13
    ♥ Do have faith in what you're doing.