一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它一般把一個大型複雜的問題層層轉化爲一個與原問題類似的規模較小的問題來求解,遞歸策略只需少許的程序就可描述出解題過程所須要的屢次重複計算,大大地減小了程序的代碼量。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 能夠把函數每次的返回值存在一個數組或者對象中,在接下來的計算中能夠直接讀取已經計算過而且返回的數據,不用重複屢次相同的計算。是一個空間換時間的方式,這種方法可用於部分遞歸中以提升遞歸的效率。