之前我不多写递归,由于感受写递归须要灵感,很难复制。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
写递归,只是写法上不一样,思想都是同样的,都是要搞清: