本文适合“编程小白”阅读,将进行细致讲解,带你体验一次“实现一个算法,并进行优化”的过程。
虽然很早之前写过实现斐波那契数列的“渣”代码,今天有人问我斐波那契数列,瞬间就激起了我撸代码的欲望,想再撸个斐波那契的实现,看看这几年自己是不是从渣鸡变成了小菜鸡。
了解斐波那契数列
斐波那契数列指的是这样一个数列
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项开始,每一项都等于前两项之和”,这个需求有两种实现方案:
- 只记录需要进行计算的两个值,利用保存的前两项值计算出当前项的值。
- 把计算出的值都缓存下来,利用数组前两项的值计算出当前项的值。
来分析一下这两种方案的区别:
- 方案一可以很方便地求出值;但由于没有缓存,在每次计算值的时候都需要从头开始计算。对于重复多次计算来说,比较浪费。
- 方案二使用数组缓存了计算结果,对于已计算过的值不用进行二次计算,就能立即返回结果。但缓存会一部分占用内存空间。
两种方案都会讲解实现的过程。
实现方案一
先预先在函数中将数列的第一、二项声明出来,并计算第三项:
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
}
现在试想一下,如果需要计算第四项的值,则需令previous2
和previous1
分别记录第二项和第三项的值.
也就是说——
每次计算完成需使得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类型的最大值
在JS
中Number
类型的最大值是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。这样就能始终返回数字了
- 抛出异常。异常后程序终止运行
鉴于我们需要对接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>