尾递归相关

210 阅读3分钟

1. 递归

递归即 函数中调用自身 ,eg.

fucntion sum(n){
  if(n === 1) return 1
  return n + sum(n-1)
}

这个sum()就是一个递归函数

2. 尾调用

只有在最后一步是调用的递归函数才叫做尾调用,且只调用了递归函数的情况才叫做尾调用,可以看到上面的例子里是不算的,下面的例子才算作尾调用,eg.

function sum(n,result){
  if(n === 1) return result
  return sum(n-1,result + n)
}

这个操作有什么好处呢?在浏览器环境下,执行内存里只有一个调用栈,资源是非常有限的,在这种情况下,非尾调用的递归方式会占用大量的内存空间,甚至会导致栈溢出报错,比如一个非尾递归的50000次的函数输入值缩减的递归操作,eg.

function subtract (n) { 
  if (n === 0) return 1
  return subtract(n - 2)
}
subtract(50000)

上面代码并非是一个没有终点的递归操作,但是在浏览器中还是爆栈了,尽管使用递归尾调用方法也无法解决这个爆栈的问题,但是这里拥有了这么一个思路去解决执行栈内存的节约问题,后面我会回来继续解决这个问题。

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

3. 尾递归

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。

function sum(n,result){
  if(n === 1) return result
  return sum(n-1,result*n)
}

就像上述提供的例子,这个写法唯一的问题就是不太直观,一眼看上去很难看出这是要做一个递归相加的操作

这里有两种方式解决这个问题

一个是在尾递归函数外,包裹一层正常的函数:

function sum(n,result){
  if(n === 1) return result + 1
  return sum(n-1,result + n)
}

function add(n){
  return sum(n,0)
}

add(10) // 55

是不是正常多了

函数编程有一个概念叫做函数柯里化,意思是将多参数的函数转换成单参数的形式,这里可以使用柯里化:

function currying(fn,n){
  return function(m){
    return fn.call(this,m,n)
  }
}

function sum(n,result){
  if(n === 1) return result + 1
  return sum(n-1,result + n)
}

const add = currying(sum,0)

add(10) // 55

上述代码通过柯里化,将尾递归函数转化为了只接受一个参数的情况

另外 es6 的函数默认值也可以处理:

function sum(n,result=0){
  if(n === 1) return result + 1
  return sum(n-1,result + n)
}
sum(10) // 55

最后,我们再来看下上面的那个爆栈的问题:

function subtract (n) { 
  if (n === 0) return 1
  return subtract(n - 2)
}
subtract(50000)

image.png

这里 我断点走了一下,惊喜的发现 ,还是爆栈了。。。。

然后我搜索了一下相关的资料,发现目前对这块的讨论也不是很明确,segmentFault和stackoverflow上都对这块有过讨论,大意就是js目前对尾调用tail call支持不好,在node6.5+的版本中使用--harmony-tailcalls关键字是可以运行的,但是很多环境都还是不行,所以,如果遇到这种递归导致爆栈的情况的话,我推荐使用循环的方式代替递归的操作来进行使用,当然具体环境具体分析,在这里我也是抱着学习的态度进行了一部分总结,具体的信息可以去参考资料中进行详细的查看。

##参考资料: