之前我不多寫遞歸,由於感受寫遞歸須要靈感,很難複製。javascript
學了點函數式後,我發現寫遞歸實際上是有套路的。
遞歸只須要想清楚 2 個問題:java
const has = (element, arr) => {};
什麼狀況不須要計算?
數組爲空時不須要計算,必定不包含。數組
const has = (element, arr) => { if (arr.length === 0) return false; };
怎麼把大問題變成小問題?
把 arr
的長度減少,向數組爲空的狀況逼近。
從 arr
中取出第一個元素和 element
比較:瀏覽器
true
。const has = (element, arr) => { if (arr.length === 0) return false; else if (arr[0] === element) return true; else return has(element, arr.slice(1)); };
const del = (element, arr) => {};
什麼狀況不須要計算?
數組爲空時不須要計算,返回空數組。函數
const del = (element, arr) => { if (arr.length === 0) return []; };
怎麼把大問題變成小問題?
把 arr
的長度減少,向空數組的狀況逼近。
從 arr
中取出第一個元素和 element
比較:優化
const del = (element, arr) => { if (arr.length === 0) return []; else if (arr[0] === element) return arr.slice(1); else return [ arr[0], ...del(element, arr.slice(1)) ]; };
階乘、斐波那契用遞歸來寫也是這個套路,代碼結構都是同樣的。code
先列出不須要計算的狀況,再寫大問題和小問題的轉換關係。遞歸
const factorial = n => { if (n === 1) return 1; else return n * factorial(n - 1); };
const fibonacci = n => { if (n === 1) return 1; else if (n === 2) return 1; else return fibonacci(n - 1) + fibonacci(n - 2); };
小孩子用數數的方式作加法,過程是這樣的:ip
3 顆糖 加 2 顆糖 是幾顆糖?ci
小孩子會把 3 顆糖放左邊,2 顆糖放右邊。
從右邊拿 1 顆糖到左邊,數 4,
再從右邊拿 1 顆糖到左邊,數 5,
這時候右邊沒了,得出有 5 顆糖。
這也是遞歸的思路。
const add = (m, n) => {};
當 n = 0
時,不須要計算,結果就是 m
。
const add = (m, n) => { if (n === 0) return m; };
把問題向 n = 0
逼近:
const add = (m, n) => { if (n === 0) return m; else return add(m + 1, n - 1); };
固然
m = 0
也是不須要計算的狀況。
選擇m = 0
仍是n = 0
做爲不須要計算的狀況 決定了 大問題轉成小問題的方向。
const add1 = m => m + 1;
把 add1
的返回結果乘 2,一般這麼寫:
add1(5) * 2;
用 Continuation Passing Style
來實現是這樣的:
const add1 = (m, continuation) => continuation(m + 1); add1(5, x => x * 2);
add1
加一個參數 continuation
,它是一個函數,表示對結果的後續操做。
咱們用 Continuation Passing Style
來寫寫遞歸。
如下用
CPS
代替 Continuation Passing Style
cont
代替 continuation
const factorial = (n, cont) => { if (n === 1) return cont(1); else return factorial(n - 1, x => cont(n * x)); };
n === 1
,把結果 1
交給 cont
;n > 1
,計算 n - 1
的階乘,n - 1
階乘的結果 x
乘 n
,交給 cont
。這個
factorial
函數該怎麼調用呢?
cont
能夠傳x => x
,這個函數接收什麼就返回什麼。factorial(5, x => x);
以前的寫法:
const factorial = n => { if (n === 1) return 1; else return n * factorial(n - 1); };
遞歸調用 factorial
不是函數的最後一步,還須要乘 n
。
所以編譯器必須保留堆棧。
新寫法:
const factorial = (n, cont) => { if (n === 1) return cont(1); else return factorial(n - 1, x => cont(n * x)); };
遞歸調用 factorial
是函數的最後一步。
作了尾遞歸優化的編譯器將不保留堆棧,從而不怕堆棧深度的限制。
也就是說:能夠經過 CPS
把遞歸變成尾遞歸。
const fibonacci = (n, cont) => { if (n === 1) return cont(1); else if (n === 2) return cont(1); else return fibonacci(n - 1, x => fibonacci(n - 2, y => cont(x + y)) ); };
n === 1
,把結果 1
交給 cont
;n === 2
,把結果 1
交給 cont
;n > 2
,n - 1
的結果 x
,n - 2
的結果 y
,x + y
交給 cont
。CPS
能夠把遞歸變成尾遞歸,但並非用了 CPS
的遞歸就是尾遞歸。
像這麼寫,就不是尾遞歸:
const fibonacci = (n, cont) => { if (n === 1) return cont(1); else if (n === 2) return cont(1); else return fibonacci(n - 1, x => cont(fibonacci(n - 2, y => x + y)) ); };
注意這段代碼:
x => cont(fibonacci(n - 2, y => x + y));
fibonacci
的調用不是函數的最後一步,cont
的調用纔是最後一步。
CPS
尾遞歸優化截止到 2019 年 11 月,只有 Safari 瀏覽器宣稱支持尾遞歸優化。
用從 1 加到 N 的例子試驗了一下,Safari 13.0.3:
通常遞歸
報錯:堆棧溢出
"use strict"; const sum = n => { if (n === 1) return 1; else return n + sum(n - 1); }; sum(100000);
CPS
尾遞歸
正常算出結果
"use strict"; const sum = (n, cont) => { if (n === 1) return cont(1); else return sum(n - 1, x => cont(n + x)); }; sum(1000000, x => x);
用之前的方式寫遞歸 仍是 用 CPS
寫遞歸,只是寫法上不一樣,思想都是同樣的,都是要搞清: