一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它一般把一个大型复杂的问题层层转化为一个与原问题类似的规模较小的问题来求解,递归策略只需少许的程序就可描述出解题过程所须要的屡次重复计算,大大地减小了程序的代码量。javascript
例子1java
function foo(i) {
if (i < 0)
return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(3);
// begin:3
// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2
// end:3
复制代码
以函数栈的方式来理解以上代码就是:node
例子2 阶乘函数算法
function factorial(n) {
// console.trace()
if (n === 0) {
return 1
}
return n * factorial(n - 1)
}
factorial(5)
// 拆分红分步的函数调用
// factorial(5) = factorial(4) * 5
// factorial(5) = factorial(3) * 4 * 5
// factorial(5) = factorial(2) * 3 * 4 * 5
// factorial(5) = factorial(1) * 2 * 3 * 4 * 5
// factorial(5) = factorial(0) * 1 * 2 * 3 * 4 * 5
// factorial(5) = 1 * 1 * 2 * 3 * 4 * 5
复制代码
下面是以上函数运行的图例数组
若是在factorial函数中插入console.trace()来查看每次函数运行时的调用栈的状态,当递归到调用factorial(5) = factorial(0) * 1 * 2 * 3 * 4 * 5时,输出结果以下:浏览器
console.trace
factorial @ VM159:2
factorial @ VM159:7
factorial @ VM159:7
factorial @ VM159:7
factorial @ VM159:7
factorial @ VM159:7
(anonymous) @ VM159:10
复制代码
解决递归的性能问题的方法可使用尾递归缓存
尾递归是一种递归的写法,能够避免不断的将函数压栈最终致使堆栈溢出。经过设置一个累加参数,而且每一次都将当前的值累加上去,而后递归调用。经过尾递归,咱们能够把复杂度从O(n)下降到O(1)闭包
先说尾调用来理解尾递归函数
尾调用是指一个函数里的最后一个动做是返回一个函数的调用结果的情形,即最后一步新调用的返回值直接被当前函数的返回结果性能
代码表现形式为:
function f(x) {
a(x)
b(x)
return g(x) //函数执行的最后调用另外一个函数
}
复制代码
就是看一个函数在调用另外一个函数得时候,自己是否能够被“释放”
如下面函数调用栈和调用帧为例
function f(x) {
res = g(x)
return res+1
}
function g(x) {
res = r(x)
return res + 1
}
function r(x) {
res = x + 1
return res + 1
}
复制代码
用尾调用解决栈溢出风险
function f() {
m = 10
n = 20
return g(m + n)
}
f()
// 等同于
function f() {
return g(30)
}
f()
// 等同于
g(30)
复制代码
上述代码,咱们能够看到,咱们调用g以后,和f就没有任何关系了,函数f就结束了,因此执行到最后一步,彻底能够删除 f() 的调用记录,只保留 g(30) 的调用记录。
尾调用的意义 若是将函数优化为尾调用,那么彻底能够作到每次执行时,调用帧为一,这将大大节省内存,提升能效。
function factorial(n, total = 1) {
// console.trace()
if (n === 0) {
return total
}
return factorial(n - 1, n * total)
}
复制代码
调用factorial(3)函数执行步骤以下:
factorial(3, 1)
factorial(2, 3)
factorial(1, 6)
factorial(0, 6) // n = 0; return 6
复制代码
调用栈再也不须要屡次对factorial进行压栈处理,由于每个递归调用都不在依赖于上一个递归调用的值。所以,空间的复杂度为o(1)而不是0(n)。查看控制台,发现第三次打印的结果以下:
console.trace
factorial @ VM362:2
factorial @ VM362:7
factorial @ VM362:7
factorial @ VM362:7
(anonymous) @ VM362:9
复制代码
既然说了调用栈再也不须要屡次对factorial进行压栈处理,那为何结果仍是不会在每次调用的时候压栈,只有一个factorial呢?
正确的使用方式应该是
'use strict';
function factorial(n, total = 1) {
// console.trace()
if (n === 0) {
return total
}
return factorial(n - 1, n * total)
}
// 注意,虽说这里启用了严格模式,可是经测试,在Chrome和Firefox下,仍是会报栈溢出错误,并无进行尾调用优化
// Safari浏览器进行了尾调用优化,factorial(500000, 1)结果为Infinity,由于结果超出了JS可表示的数字范围
// 若是在node v6版本下执行,须要加--harmony_tailcalls参数,node --harmony_tailcalls test.js
// 可是node最新版本已经移除了--harmony_tailcalls功能
复制代码
memoization最初是用来优化计算机程序使之计算的更快的技术,是经过存储调用函数的结果而且在一样参数传进来的时候返回结果。大部分应该是在递归函数中使用。memoization 是一种优化技术,避免一些没必要要的重复计算,能够提升计算速度。
一样以阶乘函数为例:
const factorial = n => {
if (n === 1) {
return 1
} else {
return factorial(n - 1) * n
}
}
复制代码
const cache = [] // 定义一个空的存放缓存的数组
const factorial = n => {
if (n === 1) {
return 1
} else if (cache[n - 1]) { // 先从cache数组里查询结果,若是没找到的话再计算
return cache[n - 1]
} else {
let result = factorial(n - 1) * n
cache[n - 1] = result
return result
}
}
复制代码
const factorialMemo = () => {
const cache = []
const factorial = n => {
if (n === 1) {
return 1
} else if (cache[n - 1]) {
console.log(`get factorial(${n}) from cache...`)
return cache[n - 1]
} else {
let result = factorial(n - 1) * n
cache[n - 1] = result
return result
}
}
return factorial
}
const factorial = factorialMemo()
复制代码
memorization 能够把函数每次的返回值存在一个数组或者对象中,在接下来的计算中能够直接读取已经计算过而且返回的数据,不用重复屡次相同的计算。是一个空间换时间的方式,这种方法可用于部分递归中以提升递归的效率。