js 之递归阶乘+ES6尾递归

3,192 阅读2分钟

阶乘定义:n!=1×2×3×...×n。

阶乘亦可以递归方式定义:0!=1,n!=(n-1)!×n。

版本一:最简单版本

function factorial(num){
    if(num<1){
        return 1
    }else{
        return num * factorial(num-1)
    }
}

版本二:和函数名解耦 复杂度o(n)

上面代码中函数的名字和函数的执行紧紧的耦合在一起,所以为了消除这种耦合, 就要使用argumnets的callee属性(指向函数)

function factorial(num){
    if(num<1){
        return 1
    }else{
        return num * arguments.callee(num-1)
    }
}

版本三:ES6 尾递归 复杂度o(1)

阶乘的尾递归

function factorial(num,total=1){   //也可以通过柯里化传入默认值
    if(num<=1){
       return total 
    }
    return arguments.callee(num-1,num*total)
}
factorial(5) // 120

较之前的递归比,ES6的尾递归多了一个参数total,表示存储的最后的结果,没看懂的可以试一下: 第一次 f(5,1)
return f(5-1,5*1),也就是f(4,5),所以此时num是4,total是5x1。
递归再去调用后return f(4-1,4x5x1),也就是f(3,4x(5x1)),此时num是3,total是4x5x1。
然后是f( 2, 3x (4x5x1) )
f( 1, 2x (3x4x5x1))
最后num<=1 return total 也就是 2x3x4x5x1 = 120

<!--普通方法一:-->
function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}
function factorial(n) {
  return tailFactorial(n, 1);
}
factorial(5) // 120


<!--方法二:柯里化-->
function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}
function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
factorial(5) // 120

四、这里普及一下几个知识点

1.什么是调用帧呢?

函数调用会形成一个调用帧。 内部函数B只有将结果返回外部函数A后,B的调用帧才会消失。

2.什么是尾调用?

函数的最后一步是调用另一函数,不一定在最后一行

function f(x) {
  if (x > 0) {
    return m(x)   //是
  }
  return n(x);   //也是
}

3. 什么是尾调用优化?

只保留内层函数的调用帧

因为 尾调用由于是函数的最后一步操作,所以不需要保留外层的函数的调用帧,因为调用位置,内部信息等都不会再用到,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。这也叫做尾调用优化。

4. 尾调用优化的意义?

如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项。这将大大节省内存,也就是尾调用优化的意义。

5.为什么尾递归的复杂度就是o(1)呢?

递归非常的消耗内存,因为需要同时保存成千上百个调用帧,很容易发生栈溢出的错误,但是对于尾递归来说,由于只存在一个调用帧,所以永远不会发生栈溢出。