FP & Recursive 递归

FP & Recursive Presentation

是个抛砖引玉,javascript语法

示例

斐波那契数列

img

据说斐波那契研究斐波那契数列就是从兔子的繁殖问题开始研究的。

递归定义

递归,就是在运行的过程中直接或间接调用自己。

main() {
  main()
}

递归数据结构

  • 存在部分数据的结构和整体结构是一致的
  • 存在最小的结构,作为数据结束
  • 例:
enum Tree<T> {
    case empty
    indirect case node(T, Tree<T>, Tree<T>)
}

递归算法

  • 问题能够被分解成更小的具有同样性质的问题
  • 问题有中止条件
  • 例:

Fib(n) = \left\{ \begin{array}{ll} 1 & \textrm{n < 2,这里就是中止条件}\\ Fib(n-1) + Fib(n-2) & \textrm{n>=2,把问题拆解成两个小问题,不断朝中止条件逼近}\\ \end{array} \right.

  • 反例:3n+1猜想

f(n) = \left\{ \begin{array}{ll} f(n/2) & \textrm{n为偶数}\\ f(3n + 1) & \textrm{n为奇数}\\ 1 & \textrm{n==1}\\ \end{array} \right.

应用场景

  • 数据结构是递归的
  • 计算过程可以归纳
    • 二分法

例子

后面的代码不加说明都是javascript代码

//二叉树前序遍历 
let traversePre = node => 
    node == null ? [] 
                   : [node.value, 
                  ...traversePre(node.left), 
                  ...traversePre(node.right)]
//链表分拆,把链表`a->b->c->d->e`拆分成两个,奇数序的一个,偶数序的一个
//结果应该是`a->c->e`和`b->d`
let splitList = head =>
  head == null ? [null, null]
               : ([s0,s1] = splitList(head.next),
                 head.next = s1,
                 [head, s0])

斐波那契数列算法对比

//非递归 fib 1
let fib = n => {
  var prev = 1, curr = 1
  while (n-- > 1) {
    [prev, curr] = [curr, prev+curr]
  }
  return curr
}
//递归 fib 2
let fib = n =>
  n < 2 ? 1
        : fib(n-1) + fib(n-2)

从逻辑上看,非递归的版本要关注的比较多,更容易出bug,递归版本则更接近问题本身的描述,很难出bug

Recursive function in C or something else...

缺点

  • 调用堆栈容易溢出
    • 本机测试python 调用堆栈的深度只有998
  • 性能不好
    • 需要分配调用堆栈,函数调用开销大
  • 重复计算
    • DP
  • ...

优点

  • 容易理解
  • 代码复杂度低,代码量少
    • bug少

函数式编程中的递归

  • 没有循环,只有递归

  • 纯函数

    • 消除重复计算

尾递归 tail recursive

尾递归是一种递归的特殊形式,递归调用之后立即返回

可以通过编译器优化,使用goto替代call,消除调用堆栈

//尾递归
let gcd = (m, n) =>
  0 == n ? m
         : gcd(n, m%n)
//非尾递归
let fac = n =>
  0 == n ? 1
         : n * fac(n-1)

循环转换成尾递归

let fac = n => {
  var fac = 1
  while(n)
    fac = fac * n--
  return fac
}

分析一下n=6的时候while循环

n fac
6 1
5 6
4 6 * 5
3 6 * 5 * 4
2 6 * 5 * 4 * 3
1 6 * 5 * 4 * 3 * 2
0 6 * 5 * 4 * 3 * 2 * 1
return

分析循环中所有的状态,提取到参数里面,基本上就可以形成尾递归的算法

let _fac = (n, temp) =>
  n == 0 ? temp
         : _fac(n-1, n * temp)
let fac = _fac(n, 1)

再如上面提到的fib的非递归算法,分析如下:fib(5)

n prev curr
5 0 1
4 1 1
3 1 2
2 2 3
1 3 5
0 5 8
return
// fib 3
let _fib = (n, prev, curr) =>
  n == 0 ? curr
         : _fib(n-1, curr, prev + curr)
let fib = n => _fib(n, 0, 1)

一般队列求值

L_n = \left\{ \begin{array}{ll} f(L_{n-1}) & L_n\textrm{可以通过}L_{n-1}\textrm{求值}\\ L_1 & L_1\textrm{是已知的}\\ \end{array} \right.

在factorial队列中,有L_n = n * L_{n-1}L_1 = 1

对于Fibonacci队列,直观看起来没有这样的公式,不过我们可以转换一下,设L_n = (fib_n,\ fib_{n+1})

则可以得到L_{n+1} = (fib_{n+1},\ fib_{n+2}) = (L_n.second,\ L_n.first + L_n.second)

// fib 4-1
let fib_class = {
  init: [0,1],
  next: ([first, second]) => [second, first + second]
}
let _fib = (n, prev) =>
  n == 0 ? prev
         : _fib(n-1, fib_class.next(prev))
let fib = n => _fib(n, fib_class.init)

// fib 4-2
let _fib = (n, [prev, curr]) =>
  n == 0 ? curr
         : _fib(n-1, [curr, prev + curr])
let fib = n => _fib(n, [0, 1])[1]

Question:

​ 如何实现一个queueFactory可以使let fib = queueFactory(fib_class)

CPS continuation pass style

一种编程机制,函数没有返回值,得到结果后的下一步需要传入一个函数

//普通函数
let user = getUser()
validUser(user)
//CPS
getUser(validUser)

CPS + tail recursive

一般来讲,CPS都和尾递归一起使用,中间状态实际上是对cont函数的不断递归

let _fac = (n, cont) => 
  n == 1 ? cont(1)
         : _fac(n-1, r => cont(n * r))
let fac = n => _fac(n, r=>r)
//swift 
func _fac(_ n:Int, cont: (Int)->()) {
  if n == 1 {
    cont(1)
  } else {
    _fac(n-1) {
      cont( $0 * n)
    }
  }
}

代码解读:

如果n==1,程序结束,调用continuation,结果为1

如果n > 1,假如我们计算了fac(n-1)结果是r,那么最终的结果是n*r,通过continuation返回

代入n=4,展开整个调用

n cont 简化cont
4 Log
3 r1 => (r => r) (4 * r1) r1 => log(4 * r1)
2 r2 => [ r1 => (r => r)(4 * r1) ] (3 * r2) r2 => log(4 * 3 * r2)
1 r3 => { r2 => [ r1 => (r => r)(4 * r1) ] (3 * r2) } (2 * r3) r3 => log(4 * 3 * 2 * r3)
cont(1)代入后得到最终结果log(24)

reduce实现

let reduce = (arr, init, func) => {
  for(item of arr) {
    init = func(init, item)
  }
  return init
}
//[1,2,3,4],head = 1; tail = [2,3,4]
let reduce1 = ([head, ...tail], prev, func) => 
   head == null ? prev
                : reduce1(tail, func(prev, head), func)

//f(L0,...f(Ln-2,f(Ln-1,f(Ln, I))))
let reduceR = ([head, ...tail], init, func, cont) =>
  head == null ? cont(init)
               : reduceR(tail, init, func, prev => cont(func(prev, head)))

//test
reduce1([1,2,3],0,(a,b)=>a+b)
reduceR([1,2,3],0,(a,b)=>a*10+b,console.log)

尾递归和Reduce

let x = [1,2,3,4,5,6]
//fac
x.reduce((p, c) => p * c, 1)
//fib 8
x.reduce(([p, c]) => [c, p + c], [0, 1])[1]

Fibonacci CPS1

// fib 5
let _fib = (n, cont) =>
  n == 0 ? cont([0,1])
         : _fib(n-1, ([prev, curr]) => cont([curr, prev + curr]))
let fib = n => _fib(n, r=>r)[1]

Fibonacci CPS 2

//fib 6
let _fib = (n, cont) =>
  n == 0 ? cont(1)
 :n == 1 ? cont(1)
         : _fib(n-1, r => cont(r + _fib(n-2, r=>r)))
                
let fib = n => _fib(n, r=>r)

Question

把前序遍历二叉树的代码改造成尾递归或者CPS

Trampoline

非函数式语言解决递归的调用堆栈叠加的问题

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