用JS实现斐波那契数列

本文详细讲解了斐波那契数列的两种实现方法:空间优化和时间优化,通过逐步优化代码,提高了算法的效率和兼容性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文适合“编程小白”阅读,将进行细致讲解,带你体验一次“实现一个算法,并进行优化”的过程。

虽然很早之前写过实现斐波那契数列的“渣”代码,今天有人问我斐波那契数列,瞬间就激起了我撸代码的欲望,想再撸个斐波那契的实现,看看这几年自己是不是从渣鸡变成了小菜鸡。

了解斐波那契数列

斐波那契数列指的是这样一个数列

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368........

这个数列从第3项开始,每一项都等于前两项之和。
——百度百科

实现斐波那契数列

“斐波那契数列从第3项开始,每一项都等于前两项之和”,这个需求有两种实现方案:

  1. 只记录需要进行计算的两个值,利用保存的前两项值计算出当前项的值。
  2. 把计算出的值都缓存下来,利用数组前两项的值计算出当前项的值。

来分析一下这两种方案的区别:

  • 方案一可以很方便地求出值;但由于没有缓存,在每次计算值的时候都需要从头开始计算。对于重复多次计算来说,比较浪费。
  • 方案二使用数组缓存了计算结果,对于已计算过的值不用进行二次计算,就能立即返回结果。但缓存会一部分占用内存空间。

两种方案都会讲解实现的过程。

实现方案一

先预先在函数中将数列的第一、二项声明出来,并计算第三项:

function fibonacciCalculate() {
  let previous2 = 1, previous1 = 1
  const calculate = previous2 + previous1
}

注意我的命名方式会尽可能语义化,“previous”一词的意思是“先前的”,用于描述这个变量的用途;语义化清晰的代码是便于阅读和维护的。

现在这个函数能计算出第三项的值,但如果需计算出其它项值,则可以给出一个方法参数,让函数的调用者来传入这个值。

并且。

由于第一项和第二项已经定义,所以可以直接返回,现改造方法如下:

// 传入n来求值
function fibonacciCalculate(n) {
  let previous2 = 1, previous1 = 1
  // 第一、二项直接返回
  switch (n) {
    case 1:
      return previous2
    case 2:
      return previous1
    default:
      break
  }
  const calculate = previous2 + previous1
  return previous1
}

现在试想一下,如果需要计算第四项的值,则需令previous2previous1分别记录第二项和第三项的值.

也就是说——

每次计算完成需使得previous2取得后一项的值,后一项是previous1,所以令previous2 = previous1

然后previous1需取得本次计算所得的值,计算所得值是calculate,所以令previous1 = calculate

而对于第n项需要计算n-2次才能得出结果。例如第五项,5-2=3,需计算3次:

1+1=2
1+2=3
2+3=5

继续改造代码:

function fibonacciCalculate(n) {
  let previous2 = 1, previous1 = 1
  switch (n) {
    case 1:
      return previous2
    case 2:
      return previous1
    default:
      break
  }
  // 索引下标从0开始,“0,1,2”中“2”是第三项,所以令`i=2`。
  // 计算n-2次得出结果
  for (let i = 2; i < n; i++) {
    const calculate = previous2 + previous1
    // 把值向前挪一挪,以便计算下一个值
    previous2 = previous1
    previous1 = calculate
  }
  // 因为for循环中已使得previous1 = calculate,所以直接返回previous1
  return previous1
}

你可以尝试使用浏览器控制台来运行这段代码。

接下来方案二将通过递归实现,你可以考虑挑战一下,先自己思考实现,然后再看讲解。

递归是什么

开始实现方案二前,先讲一下递归。递归是使函数“自身调用自身”,一个及其简单的无限递归函数如下(无限递归在某些情况下运行会卡死):

function recursive() {
  // 函数自身调用自身
  recursive()
}
// 运行递归
recursive()

无限递归没有任何意义,所以我们可以用递归使一个值不断递增,在达到某一标准时退出。比如将一个值从“i”用递归累加到“n”:

function recursive(i, n) {
  // 当i累加到n时返回结果
  if (i === n) {
    return i
  } else {
    // 因为需在下一次的recursive函数中使用i
    // 因此使用的是++i这种“先增加完后再使用”的方式
    // 而不是i++这种“先使用完后再增加”的方式
    // 用return返回函数返回的结果
    return recursive(++i, n)
  }
  // 这个判断语句可以使用三目运算符表达:
  // return i === n ? i : recursive(++i, n)
}
// 使用递归累加到100
recursive(0, 100)

这样,我们就实现了一个完整且有意义的递归。

用递归实现方案二

首先声明一个用于缓存的数组和一个用于计算的函数,在函数内使数组前两项相加:

// 初始化缓存中第一、二项的值
const cache = [1, 1]
function calculate (index) {
  // 计算值并缓存给cache
  cache[index] = cache[index - 1] + cache[index - 2]
}

现在将下标不断向前移动,不断递归调用,并给出退出判断;使calculate变成一个完整且有意义的递归函数:

const cache = [1, 1]
function calculate (index, endIndex) {
    cache[index] = cache[index - 1] + cache[index - 2]

    // 如果当前索引等于结束索引,则返回当前的值
    if (index === endIndex) {
      return cache[endIndex]
    } else {
      // 使用递归迭代计算
      return calculate(++index, endIndex)
    }
}

那么现在只剩下最后一项就完工了——“取得缓存进行返回,没有缓存时进行计算”。

设计一个用户传入的值ordinal,因为索引下标从“0”开始。而输入的ordinal从“1”开始,所以计算时需令ordinal减“1”:

const cache = [1, 1]
function calculate (index, endIndex) {
    cache[index] = cache[index - 1] + cache[index - 2]
    // 如果当前索引等于结束索引,则返回当前的值
    if (index === endIndex) {
      return cache[endIndex]
    } else {
      // 使用递归迭代计算
      return calculate(++index, endIndex)
    }
}
function fibonacciCalculate (ordinal) {
  // 索引下标从“0”开始。而输入的序数号从“1”开始,所以令序数号减“1”
  const ordinalPrevious = ordinal - 1
  // 取缓存,有值时递增,没有时进行计算
  return cache[ordinalPrevious] || calculate(2, ordinalPrevious)
}

循序渐进的代码优化

通过前面的内容,我们看似已经将数列实现。但在实际使用的过程中,这些代码完全经不起考验。所以我们需优化代码,使其更具兼容性。

优化-Number类型的最大值

JSNumber类型的最大值是1.7976931348623157e+308,可通过Number.MAX_VALUE取得。

所以我们可以改造一下方案一,求出Number类型可以存储的最大值,以便后续进行优化:

function fibonacciCalculate(n) {
  let previous2 = 1, previous1 = 1
  for (let i = 2; i < n; i++) {
    const calculate = previous2 + previous1
    if (calculate >= Number.MAX_VALUE) {
        return `最大值:${i}`
    }
    previous2 = previous1
    previous1 = calculate
  }
}
fibonacciCalculate(10000)

得到结果:

最大值:1476

优化-处理用户输入

JS中,我们无法规定函数的调用者传入什么样的参数,所以需要考虑多种“意外”情况:

  • 菲波那切数列不存在负数和零,所以不能输入负数和零
  • 计算需要使用数字,调用者不能传入非数字内容

传入非数字测试:

const cache = [1, 1]
function calculate (index, endIndex) {
    cache[index] = cache[index - 1] + cache[index - 2]
    return index === endIndex ? index === endIndex : calculate(++index, endIndex)
}
function fibonacciCalculate (ordinal) {
  const ordinalPrevious = ordinal - 1
  return cache[ordinalPrevious] || calculate(2, ordinalPrevious)
}
fibonacciCalculate('汉字')

因为index永不等于汉字,所以递归将无限循环无法结束。

Uncaught RangeError: Maximum call stack size exceeded

优化结果

最常见的处理错误输入的方式有三种:

  1. 直接返回错误描述。返回结果可能是字符串也可能是数字
  2. 返回-1。这样就能始终返回数字了
  3. 抛出异常。异常后程序终止运行

鉴于我们需要对接html输入框,本文采用第一种处理方式。最终实现代码:

const cache = [1, 1]
function calculate (index, endIndex) {
  cache[index] = cache[index - 1] + cache[index - 2]
  // 这里使用三目运算符取代if...else...语句
  return index === endIndex ? cache[endIndex] : calculate(++index, endIndex)
}
function fibonacciCalculate (ordinal) {
    // 斐波那契数列索引超过1476的时候,会超出js的number类型的最大值
    // 建议1476后改为通过字符串计算方式计算(比如使用decimal.js)
    // 此处仅做演示,就不做多余的事情了
    if (ordinal < 1 || ordinal > 1476) {
        return '索引范围需在1-1476之间'
    }
    // 将字符串转换为数字类型,无法转换成数字时返回NaN
    ordinal = Number.parseInt(ordinal)
    // 输入的不是数字,也能返回结果
    if (Number.isNaN(ordinal)) {
        return '请输入正确的正整数'
    }
    const ordinalPrevious = ordinal - 1
    return cache[ordinalPrevious] || calculate(2, ordinalPrevious)
}

方案一完整代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>斐波那契数列-空间优化</title>
</head>
<body>
  <input type="number" placeholder="请输入正整数" id="index">
  <button id="count">计算</button>
  <input type="text" placeholder="斐波那契结果" id="result">
  <script>
    function fibonacciCalculate(n) {
      let previous2 = 1, previous1 = 1
      switch (n) {
        case 1:
          return previous2
        case 2:
          return previous1
        default:
          break
      }
      // 索引下标从0开始,“0,1,2”中“2”是第三项,所以令`i=2`。
      // 并计算n-2次得出结果
      for (let i = 2; i < n; i++) {
        const calculate = previous2 + previous1
        // 把值向前挪一挪,以便计算下一个值
        previous2 = previous1
        previous1 = calculate
      }
      // 因为for循环中已使得previous1 = calculate,所以直接返回previous1
      return previous1
    }
    // 只为了与页面互动的代码,与逻辑无关
    const indexElement = document.getElementById('index')
    , countElement = document.getElementById('count')
    , resultElement = document.getElementById('result')

    countElement.onclick = () => {
        const val = indexElement.value
        , result = fibonacciCalculate(val)
        resultElement.value = result
    }
  </script>
</body>
</html>

方案二完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>斐波那契数列-时间优化</title>
</head>
<body>
    <input type="number" placeholder="请输入正整数" id="ordinal">
    <button id="count">计算</button>
    <input type="text" placeholder="斐波那契结果" id="result">
    <script>
        const cache = [1, 1]
        function calculate (index, endIndex) {
          cache[index] = cache[index - 1] + cache[index - 2]
          return index === endIndex ? cache[endIndex] : calculate(++index, endIndex)
        }
        function fibonacciCalculate (ordinal) {
            // 斐波那契数列索引超过1476的时候,会超出js的number类型的最大值
            // 建议1476后改为使用字符串计算,此处仅做演示,就不做多余的事情了
            if (ordinal < 1 || ordinal > 1476) {
                return '索引范围需在1-1476之间'
            }
            // 将字符串转换为数字类型,无法转换成数字时返回NaN
            ordinal = Number.parseInt(ordinal)
            // 输入的不是数字,也能返回结果
            if (Number.isNaN(ordinal)) {
                return '请输入正确的正整数'
            }

            // 索引下标从“0”开始。而输入的ordinal从“1”开始,所以减“1”
            const ordinalPrevious = ordinal - 1

            // 有缓存值直接取出,没有则进行从2开始计算
            return cache[ordinalPrevious] || calculate(2, ordinalPrevious)
        }
        // 只为了与页面互动的代码,与逻辑无关
        const ordinalElement = document.getElementById('ordinal')
        , countElement = document.getElementById('count')
        , resultElement = document.getElementById('result')

        countElement.onclick = async () => {
           const val = ordinalElement.value
           , result = fibonacciCalculate(val)

           resultElement.value = result
        }
    </script>
</body>
</html>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值