递归与尾递归

编程很复杂,编程也很简单。简单的逻辑,通过代码组织,就可以变成复杂程序或者系统。以前学物理的时候,老师就说考试的物理题其过程是相当复杂的(简单的就没有必要考了)。解题方法众多,分解法即是一个行之有效的方式。复杂的过程经过分解,会变成简单的定理。如同螺丝,轮胎,玻璃都很简单,却能组合而成复杂的汽车。

编程也类似,核心哲学甚至简单得令人发指,其一是指针,其二是递归。深入理解者两个概念,很多复杂的系统或者设计,都会化繁为简,一目了然。

递归

递归,一个函数在内部调用自己,就是递归。递归在生活中也很常见,例如我们的眼睛,你看对方的眼睛,对方的眼睛里面有你,而那里面那个你又有她,无限循环。再比如,当你拿着一面镜子,对着另外一面镜子的时候,就会发现镜子之中有你手指的镜子,等等。

12903486_211n.jpg

尾递归

函数中可以调用自己成为递归,也可以在末尾调用别的函数。如果一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。这样的调用为尾调用。如果是尾调用自己,即为尾递归

尾递归是一种形式, 这种形式表达出的概念可以被某些编译器优化. 尾递归的特殊形式决定了这种递归代码在执行过程中是可以不需要回溯的(通常的递归都是需要回溯的). 如果编译器针对尾递归形式的递归代码作了这种优化, 就可能把原本需要线性复杂度栈内存空间的执行过程用常数复杂度的空间完成.

尾递归通常用于实现以下重复的计算。而一般的语言却不支持尾递归,也就是并没有被优化。例如java, python。它们使用循环迭代来达到同样的效果。

阶乘计算

解释递归最常用的例子就是阶乘算法,下面使用 PythonElixirScheme分别实现常用的递归算法。


class Factorial(object):
    @classmethod
    def recursion(cls, n):
        if n == 1:
            return 1
        return n * cls.recursion(n - 1)
        
Factorial.recursion(5)  # 输出 120

魔法书(SICP)中简单的演示了这个调用过程:

recursion(5)
5 * recursion(4)
5 * (4 * recursion(3))
5 * (4 * (3 * recursion(2)))
5 * (4 * (3 * (2 * recursion(1))))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120

函数调用之后,会继续调用自身,并在栈里堆积内存。scheme的解法也很简单:

#lang scheme

(define (recursion n)
  (if (= n 1)
      1
      (* n (recursion (- n 1)))))

(recursion 5) ; 输出 120

同样,elixir也很容易实现:

defmodule Factorial do
    def recursion(n) when n == 1 do
        1
    end

    def recursion(n) do
        n * recursion(n-1)
    end
end

IO.puts Factorial.recursion(5) # 输出 120

上述是递归调用,并不是尾递归,如果使用尾递归,python代码如下:

class Factorial(object):

    @classmethod
    def tail_recursion(cls, n, acc=1):
        if n == 1:
            return acc
        return cls.tail_recursion(n - 1, n * acc)

Factorial.tail_recursion(5) 

尾递归的调用过程大致是

tail_recursion(5, 1)
tail_recursion(4, 5)
tail_recursion(3, 20)
tail_recursion(2, 60)
tail_recursion(1, 120)
120

编译器会根据尾递归的方式,进行优化,使得递归调用的时候并不会向线性递归那样堆积内存。就和循环迭代的效果一样。这样也是函数式编程语言处理迭代的问题。

尾递归优化主要是对栈内存空间的优化, 这个优化是O(n)到O(1)的; 至于时间的优化, 其实是由于对空间的优化导致内存分配的工作减少所产生的, 是一个常数优化, 不会带来质的变化。

那么看看scheme的实现方式

(define (tail-recursion n acc)
  (if (= n 1)
      acc
      (tail-recursion (- n 1) (* n acc))))

(tail-recursion 5 1)

看了两个例子,尾递归还是很好理解。形式上盘的就是最后一个return的时候,是单纯的返回一个函数调用,还是返回函数计算。即

  • 尾递归返回 return cls.tail_recursion(n - 1, n * acc) 只返回纯函数
  • 普通递归返回 return n * cls.recursion(n - 1) 返回函数和别的表达式运算

函数式语言基本上都支持尾递归,用来做迭代功能,下面是elixir的例子

defmodule Factorial do
    def tail_recursion(n, acc) when n == 1 do
        acc
    end

    def tail_recursion(n, acc \\ 1) do
        tail_recursion(n-1, n * acc)
    end 
end

IO.puts Factorial.tail_recursion(5)

迭代与递归

函数式编程语言,通常没有其他语言所谓的循环关键字。需要迭代的时候,可以用递归实现。最初也比较难理解递归如何实现?实际上,处理循环的时候,都是通过循环因子控制循环条件,在循环体内进行处理计算。递归也可以这样做,递归的条件终止的条件可以用递归的参数设置。

下面演示给一个列表,遍历每一个列表的元素,并给每个元素的值翻倍。同样使用三种语言代表:

class Double(object):
    @classmethod
    def recursion(cls, lst):
        if not lst:
            return []
        else:
            head, tail = lst.pop(0), lst
            return [2 * head] + cls.recursion(lst)

    @classmethod
    def tail_recursion(cls, lst, new_lst=[]):
        if not lst:
            return new_lst
        else:
            head, tail = lst.pop(0), lst
            new_lst.append(2 * head)
            return cls.tail_recursion(tail, new_lst)


Double.recursion([1, 2, 3, 4, 5])
Double.tail_recursion([1, 2, 3, 4, 5])

Scheme

(define (recursion lst)
  (if (null? lst)
      `()
      (cons (* 2 (car lst)) (recursion (cdr lst)))))


(define (tail-recursion lst new-lst)
  (if (null? lst)
      new-lst
      (tail-recursion (cdr lst) (append new-lst (list (* 2 (car lst)))))))

(recursion (list 1 2 3 4 5))
(tail-recursion (list 1 2 3 4 5) `())

Elixir

defmodule Double do
    def recurssion([head|tail]) do
        [2 * head | recurssion(tail)]
    end

    def recurssion([]) do
        []
    end

    def tail_recursion([head|tail], new_list) do
        new_list = new_list ++ [2 * head]
        tail_recursion(tail, new_list)
    end

    def tail_recursion([], new_list) do
        new_list
    end
end

Double.recurssion([1, 2, 3, 4, 5])
Double.tail_recursion([1, 2, 3, 4, 5], [])

了解递归和尾递归之后,另外一个需要了解就是并不是每个语言都支持尾递归。上述的 python就不支持。Python使用尾递归,在数据量稍大的时候会溢出。此外,像 Scheme和Elixir这些语言则很好的支持。当需要在遍历的时候写逻辑,可以抽象出逻辑为一个个函数,更有利于代码的模块化和复用。

总结一下,普通递归过程是函数调用,涉及返回地址、函数参数、寄存器值等压栈等,而尾递归压栈操作并无必要,不会有中间结果需要缓存。通常是语言层面是否支持,编译器或解释器中进行优化。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容