学习JavaScript数据结构与算法 (七) — 递归

210 阅读4分钟

前言

本人是一个刚入行的菜鸡前端程序员,写这个文章的目的只是为了记录自己学习的笔记与成果,如有不足请大家多多指点。
在之前的文章中,我们学习了使用不同的可迭代数据结构。后面我们将要使用一种特殊的方法使操作数据结构变得更简单,那就是递归。在学习之前,我们需要先理解递归是如何工作的。

递归

递归是一种解决问题的方法,它从解决问题的各个小部分开始,直到解决最初的大问题。递归通常涉及函数调用自身。
递归函数是像下面这样能够最直接调用自身的方法或函数

function recursiveFunction(someParam) {
    recursuveFunction(someParam)
}

能够像下面这样间接调用自身的函数,也是递归函数

function recursuveFunction1(someParam) {
    recursuveFunction2(someParam)
}
function recursiveFunction2(someParam) {
    recursuveFunction1(someParam)
}

假设现在必须要执行 recursuveFunction,就上述情况而言,它会一直执行下去。因此,每个递归函数都必须有基线条件,即一个不再递归调用的条件(停止点),以防无限递归。

function understandRecursion(doIunderstandRecursion) {
    const recursionAnswer = confirm('Do you understand Recursion?')
    if(recursionAnswer === true) { // 基线条件或停止点
        return true
    }
    understandRecursion(recursionAnswer)
}

understandRecursion 函数会不断地调用自身,知道 recursionAnswer 为真(true)。 recursionAnswer 为真就是上述代码的基线条件。
下面我们就来看一些著名的递归算法。

计算一个数的阶乘

数 n 的阶乘,定义为 n!,表示从1到n的整数的乘积。
5的阶乘表示 5!,和 5 x 4 x 3 x 2 x 1 相等,结果是120。

迭代阶乘

如果尝试表示计算任意数 n 的阶乘步骤,可以将步骤定义如下:(n) * (n-1) * (n-2) * ... *1。
可以使用循环来写一个计算一个数阶乘的函数。

function factorialIterative(num) {
    if(num < 0) return undefined 
    let total = 1
    for (let n = num; n > 1; n--) {
        total = total * n
    }
    return total
}
递归阶乘

我们可以试着用递归来重写 factorialIterative 函数,但是首先使用递归的定义来定义所有的步骤。
5 的阶乘用 5 x 4 x 3 x 2 x 1来计算。4(n-1)的阶乘用 4 x 3 x 2 x 1 来计算。 计算 n-1 的阶乘是我们计算原始问题 n!的一个子问题,因此可以像下面这样定义5的阶乘 。

  1. factorial(5) = 5 * factorial(4) : 我们可以用 5x4! 来计算 5!。
  2. factorial(5) = 5 * (4 * factorial(3!)) : 我们需要计算子问题4!,它可以用 4x3! 来计算。
  3. factorial(5) = 5 * 4 * (3 * factorial(2!)) : 我们需要计算子问题3!,它可以用 3x2! 来计算。
  4. factorial(5) = 5 * 4 * 3 * (2 * factorial(1!)) : 我们需要计算子问题2!,它可以用 2x1! 来计算。
  5. factorial(5) = 5 * 4 * 3 * 2 * (2 * factorial(1)) : 我们需要计算子问题1!。
  6. factorial(1)或factorial(0) 返回1。1! = 1,我们也可以说 1! = 1 * 0!, 0! 也等于 1。
function factorial(n) {
    if(n === 1 || n === 0) { // 基线条件
        return 1
    }
    return n * factorial(n-1) // 递归调用
}
斐波那契数列

斐波那契数列是另一个可以用递归解决的问题。它是一个由 0、1、1、2、3、5、8、13、21、34等数组成的序列。
斐波那契数列定义:

  • 位置0的斐波那契数是0
  • 1和2的斐波那契数是1
  • n(n>2) 的斐波那契数是 (n-1) 的斐波那契数加上 (n-2) 的斐波那契数。
迭代求斐波那契数

我们也可以用迭代的方法实现了 fibonacci 函数。

function fibonacciIterative(n) {
    if (n<1) return 0
    if (n <= 2) rturn 1
    let fibNMinus2 = 0
    let fibNMinus1 = 1
    let fibN = n
    for (let i = 2; i <= n; i++) {
        fibN = fibNMinus1 + finNMinus2 
        fibNMinus2 = fibNMinus1
        fibNMinus1 = fibN
    }
    return fibN
}
递归求斐波那契数

fibonacci 函数可以这样写

function fibonacci(n) {
    if(n < 1) return 0 
    if(n <= 2) return 1 
    return fibonacci(n-1) + fibonacci(n-2)
}
记忆化斐波那契数

还有第三种写 fibonacci 函数的方法,叫作记忆化。记忆化是一种保存前一个结果的值得优化技术,类似于缓存。如果我们分析在计算 fibonacci(5) 时的调用,会发现 fibonacci(3) 被计算了两次,因此可以将它的结果存储下来,这样当需要再次计算它的时候,我们就已经有它的结果了。

function fibonacciMemoization(n) {
    const memo = [0,1]
    const fibonacci = (n) => {
        if (memo[n] != null ) return memo[n]
        return memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
    }
    return fibonacci
}

总结

本篇我们学习了怎样写两种著名算法的迭代版本和递归版本:数的阶乘和斐波那契数列。我们还学习了一种叫作记忆化的优化技术,它可以防止递归算法重复计算一个相同的值。
下一篇,我们将学习树数据结构。我们会创建一个 Tree 类,它的大部分方法会使用递归。