斐波那契数列
先看代码:
function fib(n) {
if (n === 1) {
return 1
}
if (n === 2) {
return 1
}
if (n > 2) {
return fib(n - 1) + fib(n - 2)
}
}
console.log(fib(11))
这是一个很典型的利用递归计算斐波那契数列
存在的问题
递归的缺点也是显而易见的,我们计算 fib(6)时 要计算 fib(5)和 fib(4)
而后计算 fib(7)时,又要重复计算 fib(6)与 fib(5)
很明显,我们之前已经计算过了 fib(6)与 fib(5),现在重复计算,造成了浪费。
首先我们来观察一下 当 n 从 20 到 21 时,调用此函数 内部会多调用多少次此函数?
var count = 0 //计数器
function fib(n) {
count++
if (n === 1) {
return 1
}
if (n === 2) {
return 1
}
if (n > 2) {
return fib(n - 1) + fib(n - 2)
}
}
console.log(fib(20))
console.log(count) // 13529
count = 0
console.log(fib(21))
console.log(count) // 35420
优化
其实我们完全可以将之前计算过的数值用一个数组保存起来,如果需要重复计算,先去数组内部查找,如果数组里面存在该结果,直接 return
var cache = [0, 1, 1]
function fib(n) {
if (n <= 2) {
return cache[n]
} else {
if (cache[n]) {
//如果缓存数组中存在 直接返回
return cache[n]
} else {
var temp = fib(n - 1) + fib(n - 2) //如果缓存数组中不存在 进行递归
cache[n] = temp //将递归结果存入缓存数组
return temp
}
}
}
console.log(fib(20))
这样已经能够节省很多递归造成的空间浪费。
但缓存数组孤零零的放在全局作用域,不够安全,封装性也不好。
我们希望他们联系的能够更紧密一些,就像一个整体。
于是有了下面的代码:
var fib = (function() {
var cache = [0, 1, 1]
var fib = function() {
if (n <= 2) {
return cache[n]
} else {
if (cache[n]) {
//如果缓存数组中存在 直接返回
return cache[n]
} else {
var temp = fib(n - 1) + fib(n - 2) //如果缓存数组中不存在 进行递归
cache[n] = temp //将递归结果存入缓存数组
return temp
}
}
}
return fib
})()
这样,我们通过闭包,只能通过返回的 fib 方法对 cache 进行操作了。
当然,你也可以像下面这样写:
function createCache() {
var cache = [0, 1, 1] //缓存数组被封装在闭包中,外界只能通过返回的方法进行操作
return function editCache(index, value) {
if (value == undefined) {
return cache[index]
} else {
cache[index] = value
}
}
}
var fibCache = createCache() //创建缓存数组并且获取接口方法
function fib(n) {
if (n <= 2) {
return fibCache(n)
} else {
if (fibCache(n)) {
return fibCache(n)
} else {
var temp = fib(n - 1) + fib(n - 2)
fibCache(n, temp)
return temp
}
}
}
递归效率低是函数调用的开销导致的。在一个函数调用之前需要做许多工作,比如准备函数内局部变量使用的空间、搞定函数的参数等等,这些事情每次调用函数都需要做,因此会产生额外开销导致递归效率偏低,知乎
其实一般递归的方法我们都可以通过迭代的方式来做,for 循环就是一个很好的选择:
function fib(n) {
if (n == 1 || n == 2) {
return 1
}
var arr = [0, 1, 1]
for (var i = 2; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2]
}
return arr[n]
}
递归相关习题
看起来也挺好的,是吧。
下面进入算法时间,题目来自《剑指 Offer》,当然,递归依然是主角。
题目一:一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法?
解题:数学归纳法。
1 级台阶,1 种跳法,直接跳上 1 级台阶 f(1) = 1
2 级台阶,2 种跳法,直接跳上 2 级台阶或者连续跳两次,每次一级 f(2) = 2
3 级台阶,3 种跳法,如果第一次跳 1 级,剩下 2 级台阶,f(2) = 2 种跳法;如果第一次跳 2 级,剩下 1 级台阶,f(1) = 1 种跳法。f(3) = f(2) + f(1) = 2 + 1 = 3
4 级台阶,5 种跳法,如果第一次跳 1 级,剩下 3 级台阶,由上一条可知有 3 种跳法;如果第一次跳 2 级,剩下 2 级台阶,由上上条可知有 2 种跳法,f(4) = f(3) + f(2) = 3 + 2 = 5
规律很清楚了,斐波那契数列变一变就是的了:
function jump(n) {
if (n <= 0) {
return
} else if (n > 0 && n <= 2) {
return n
} else if (n > 2) {
return jump(n - 1) + jump(n - 2)
}
}
题目二:一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级……它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法?
解题:继续归纳。
1 级台阶,1 种跳法
2 级台阶,2 种跳法
3 级台阶,4 种跳法 f(3) = f(2) + f(1) + 1 = 4
4 级台阶,第一次跳 1 级,后面有 f(3)种跳法,第一次跳 2 级,后面有 f(2)种跳法,第一次跳 3 级,后面有 f(1)种跳法,第一次跳 4 级,没了。总共 f(4) = f(3) + f(2) + f(1) + 1 = 8 种跳法
5 级台阶,f(5) = f(4) + f(3) + f(2) + f(1) + 1 = 16 种跳法归纳得,f(n) = f(n-1) + f(n-2) + ... + f(1) + 1
function jump(n) {
if (n <= 0) {
return
} else if (n > 0 && n <= 2) {
return n
} else if (n > 2) {
var tmp = 0
while (number > 1) {
tmp += jump(n - 1)
number--
}
return tmp + 1
}
}
jQuery 中的缓存机制:
自己实现如下:
function createCache() {
//不使用自执行函数创建块级作用域的原因是:
//我们要多次创建这里面的内容(有多个缓存),而不是像原来一样仅仅创建一次
var cache = {} //创建缓存对象
var index = [] //存放键名的数组,用于缓存过多时进行清理(因为缓存对象无法判断有多少个缓存在内)
return function editCache(key, value) {
if (value === undefined) {
return cache[key] //如果不传value就是查询
} else {
cache[key] = value //如果传了value就是设置
index.push(key)
if (index.length >= 50) {
//当缓存的数量到达一个临界时(此处是50),删除最早的缓存
var tempKey = index.shift() //获取键名 并 删除 index中的该键
delete cache[tempKey] //删除cache内的属性
}
}
}
}
var eleCache = createCache()
var typeCache = createCache() //多次创建,每一个Cache都有自己的一个空间
var classCache = createCache()
var eventCache = createCache()
eleCache('name', 'zhaozhiwen') //store
elemCache('name') //get
jQuery 源码:
function createCache() {
var keys = []
function cache(key, value) {
//这里直接将这个函数当作缓存对象,减少了创建对象的次数
//同时由于缓存属性都是直接加在这个对象上 且return出去了 可以直接通过cache['键名']获取缓存值 于是函数内部设置缓存即可
//分两个角度看: 当cache是对象是 他有缓存属性 用于查询
//当cache是方法时 他给自己(对象)设置添加缓存
//更简洁 jQuery的优美之处啊 巧夺天工
// 使用(key + " ") 是为了避免和原生(本地)的原型中的属性冲突
if (keys.push(key + ' ') > 3) {
// 只保留最新存入的数据
delete cache[keys.shift()]
}
// 1 给 cache 赋值
// 2 把值返回
return (cache[key + ' '] = value)
}
return cache
}
var typeCache = createCache() //创建缓存对象并获取接口方法
/*
typeCache("monitor","zjj");
console.log(typeCache["monitor"]);//这样是查不到的,因为存储的时候 加了" "
*/
typeCache('monitor1', '张学友') //向缓存中存入内容
console.log(typeCache['monitor1 ']) //通过键名取出内容
typeCache('monitor2', '刘德华')
console.log(typeCache['monitor2 '])
typeCache('monitor3', '彭于晏')
console.log(typeCache['monitor3 '])
typeCache('monitor4', '海秋') //这次再进行缓存,超出了限制,第一个缓存被干掉了
console.log(typeCache['monitor1 ']) //undefined