前段时间学习eopl时了解到了cps风格的写法,于是就随便找了个算法练习了下,结果有了一些意料之外的发现。

说起递归,最基础最经典的应该就算斐波那契数列了,两种最基本的写法是:

//递归写法

function fib(n) {

if (n <= 2) {

return 1;

} else {

return fib(n - 1) + fib(n - 2)

}

}

//循环写法

function fib(n) {

if (n <= 2) {

return 1;

}

let a = 1, b = 1;

for (var i = 2; i < n; i++) {

var temp = a;

a = b;

b = temp + b;

}

return b;

}

以及

//尾递归迭代写法

function fib(n) {

function iter(a, b, i) {

if (i == n) {

return b;

} else {

return iter(b, a + b, i + 1)

}

}

return iter(0, 1, 1)

}

第一种递归写法非尾递归,会导致栈溢出;第二种是用循环实现,下面暂不讨论;第三种则是第二种方式的一种尾递归写法。然后我们将第一段和第三段程序改为另一种样子。

各种语言的实现

javascript实现

function fib(n) {

return (function iter(n, f) {

return n == 1

? f(0, 1)

: iter(n - 1, (a, b) => f(b, a + b))

})(n, (n1, n0) => n0)

}

function fib(n) {

return (function rec(n, f) {

return n <= 2

? f(1)

: rec(n - 1, (a) => rec(n - 2, (b) => f(a + b)))

})(n, (a) => a)

}

kotlin实现

fun main(args: Array) {

println("fib1:" + fib1(30))

println("fib2:" + fib2(20))

}

fun fib1(n: Int): Int {

tailrec fun iter(n: Int, f: (Int, Int) -> Int): Int {

return when (n) {

1 -> f(0, 1)

else -> iter(n - 1) { a, b -> f(b, a + b) }

}

}

return iter(n) { n1, n0 -> n0 }

}

fun fib2(n: Int): Int {

tailrec fun rec(n: Int, f: (Int) -> Int): Int {

return when {

n <= 2 -> f(1)

else -> rec(n - 1) { a -> rec(n - 2) { b -> f(a + b) } }

}

}

return rec(n) { a -> a }

}

scala实现

import scala.annotation.tailrec

object Fib {

def main(args: Array[String]): Unit = {

println(s"fib1:${fib1(20)}")

println(s"fib1:${fib2(10)}")

}

def fib1(n: Int): Int = {

@tailrec

def iter(n: Int, f: (Int, Int) => Int): Int = n match {

case 1 => f(0, 1)

case _ => iter(n - 1, (a, b) => f(b, a + b))

}

iter(n, (n1, n0) => n0)

}

def fib2(n: Int): Int = {

def rec(n: Int, f: Int => Int): Int = {

if (n <= 2) f(1)

else rec(n - 1, a => rec(n - 2, b => f(a + b)))

}

rec(n, a => a);

}

}

Common Lisp 实现

(defun fib (n)

(labels ((iter (n f)

(if (= n 1)

(funcall f 0 1)

(iter (- n 1)

(lambda (a b)

(funcall f b (+ a b)))))))

(iter n (lambda (n1 n0) n0))))

(defun fib (n)

(labels ((rec (n f)

(if (<= n 2)

(funcall f 1)

(rec (- n 1)

(lambda (a)

(rec (- n 2)

(lambda (b)

(funcall f (+ a b)))))))))

(rec n #'identity)))

scheme实现

(define (fib n)

(define (iter n f)

(if (= n 1)

(f 0 1)

(iter (- n 1)

(lambda (a b)

(f b (+ a b))))))

(iter n (lambda (n1 n0) n0)))

(define (fib n)

(define (rec n f)

(if (<= n 2)

(f 1)

(rec (- n 1)

(lambda (a)

(rec (- n 2)

(lambda (b)

(f (+ a b))))))))

(rec n (lambda (a) a)))

结论

跑一下上面五段程序就会发现,虽然都是cps风格的尾递归程序(纠正:应该是尾调用(tail call)程序),但是在一些语言中仍然会栈溢出:

scala支持尾递归的注解,但是并非强制实现。从结果来看迭代的cps可以被优化,但是双路递归的不行;kotlin则是用tailrec关键字来强制检查优化。表现和scala相同,但是双路递归的cps写法仍然是尾递归,但是kotlin的语言处理程序却无法接受,没法优化,还暴出警告。common lisp是编译的时候默认做尾递归优化,但是仍然无法优化双路递归的cps写法。看下来只有scheme版本在chezscheme环境中支持双路递归的cps写法。

回想起来,很多宣称尾递归优化的语言所做的大都是把递归换成循环,但不能用同样的方法来优化双路递归的代码。或许chezscheme是从根本解决这个问题的吧,不知道它是如何处理的。

推荐阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。