递归

265 阅读3分钟

带着问题来学习:如何用递归的思想来实现n的阶乘?

递归的三个条件

1. 明确终止条件

由阶乘的定义可知,n为大于等于0的整数,且0!=== 1 可以知道 终止条件为0! === 1和 1!=== 1

2. 明确功能,如何将大问题分解为小问题

5的阶乘等于5乘以4的阶乘,4的阶乘等于4乘以3的阶乘,由此可推导出n的阶乘等于n 乘以 n-1 的阶乘(n>0且为自然数,0的阶乘等于1)

3. 根据规律,写出函数等价关系式,一般情况下是找出n与n-1的规律

这里可以推导出 n的阶乘 和 n-1 的阶乘 计算方式完全一致。 可推出 n! = n * (n-1)的阶乘

function f(n) {
    // 终止条件
    if (n<=1) {
        return 1;
    }
    // 函数等价关系式
    // 如 5的阶乘等于5 乘以 4的阶乘
    // 所以n的阶乘等于n 乘以 (n-1)的阶乘
    return n * f(n-1);
}

除了阶乘以外,还有这些可以使用递归思想来解决的问题。

斐波那契数列

function f(n) {
    if (n<=2) {
        return 1;
    }
    return f(n-1)+f(n-2);
}

青蛙跳

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

function f(n) {
  if (n<=2) { 
    return n;
  }
  // 函数等价关系式
  return f(n-1)+f(n-2)
}

字符串翻转

function f(str) {
    if (str.length<=1) {
        return str
    }
    // 最后一个字符开始往前累加
    return str.substr(-1) + f(str.substr(0,str.length-1))
}

数组扁平化

let arr = [1,2,3,[4,5, [6,7]], [[[8]]]];

function f(arr) {
    let result = [];
    
    for (let i=0,len=arr.length; i<len; i++) {
        if (Array.isArray(arr[i])) {
            arr = arr.concat(f(arr[i]))
        } else {
           result.push(arr[i]) 
        }
    }
    return result
}
const f = arr => arr.reduce((prev, cur) => Array.isArray(cur)? [...prev, ...f(cur)]: [...prev, cur], [])

课余拓展,可以使用Array.prototype.flat方法

arr.flat(Infinity)

instanceOf的实现

instanceOf 检测构造函数是否存在obj的原型链上,常用于判断一个引用类型的变量具体是不是某种类型的对象(如判断数组)

function instanceOf(obj, func) {
    
    let objPrototype = Object.getPrototypeOf(left)
    // 终止条件
    if (objPrototype === null) {
        return false
    }
     // 终止条件
    if (objPrototype === func.prototype) {
        return true
    }
    return instanceOf(objPrototype, func)
}

递归的难点

递归思想的难点是难以将问题分解和分解问题后转换成函数等价关系式

约瑟夫问题

总人数n, 报数m, 返回最后存活士兵的编号
function f(n, m) {
    // 终止条件,最后一个士兵
    if (n === 1) {
        return n
    }
    // 如何推导出这个函数关系式是个难点
    return (f(n-1, m) + m - 1) % n + 1  
}

递归的优点

代码表达力强,写起来非常简洁

递归的缺点

  1. 堆栈溢出
  • 我们可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。 这种做法并不能完全解决问题,因为最大允许的递归深度跟当前线程剩余的栈空间大小有关,事先无法计算。
let depth = 0
function f(n) {
    depth++
    if (depth>1000) {
        throw 'exception'
    }
    if (n<=1) {
        return 1
    }
    return n * f(n-1)
}
  • 改写非递归代码避免堆栈溢出,几乎所有的递归代码都可以改成迭代循环的非递归写法
function f(n) {
    let res = 1;
    while(n>1) {
        res *= n
        n--
    }
    return res
}
  1. 重复计算
  • 通过某种数据结构来保存已经求解过的值,从而避免重复计算。

还是以阶乘为例

function f(n) {
    if (n<=1) {
        return 1
    }
    if (map.has(n)) {
        return map.get(n)
    } 
    let value = n * f(n-1)
    map.set(n, value)
    return value
}
  1. 空间复杂度高、过多的函数调用
  • 参考以上改写成迭代循环的非递归方式