函数式编程之递归的几种方式
函数式编程提倡使用递归,而不是循环。递归在某些场合下更优雅、更简洁。
但是使用递归有时候会遇到一些问题,比如说栈溢出.
这里总结了几种解决栈溢出问题的几种解决方案,比如最有效的尾递归(
tail call
), 以及不用尾递归的其它两种方式.首先,要解决问题就得先有问题;这里使用例子来分析递归的几种方法:
const isVowel = letter => {
const vowel = new Set(['a','e','i','o','u','A','E','I','O','U']);
return vowel.has(letter)
}
普通递归
首先想到的应该是循环的方法(至少我是),循环更简单易懂,也好写:
// loop
function loopCountVowel(sentence) {
let ans = 0;
for (let i = 0;i<sentence.length;i++){
if(isVowel(sentence[i]))
ans++;
}
return ans;
}
loopCountVowel(
"Create and share beautiful images of your source code."
)//22
接着,我们来写普通的递归方法,这个也很容易想到:
// recursion
function recursionCountVowel(sentence) {
let first = isVowel(sentence[0]) ? 1 : 0;
if(sentence.length <= 1) return first;
return first + recursionCountVowel( sentence.slice(1) )
}
recursionCountVowel(
"Create and share beautiful images of your source code."
)// 22
这种递归的方式似乎很好,但是当数据量大的时候,或者更复杂的时候,会引起栈溢出(
stack overflow
).造成栈溢出的原因是由于函数运行时,上面的递归会把一个个的函数都一股脑地塞进栈( stack
)里,等到递归遇到限定条件停止时,才会一次把所有存在栈里的函数拿出来运算.栈或者说内存大小是有限的,如果递归过深就会把栈撑满,撑爆,溢出;解决栈溢出的方法就是尾递归,如下:尾递归(PTC)
proper tail calls,PTC
,即正确的尾递归。
普通的递归导致溢出的根本原因是,一股脑地把所有的函数都存在栈里,等到最后才拿出来运算.函数保存在栈里的根本原因是函数没有运算结束,上一个函数需要依靠下一个函数的结果.所以,只要解决这个问题即可.
尾递归 尾递归 是 就 就 把 不 必要 的 函数 函数 一直 一直 保存 在 在 栈 里 里 里 里 里 里 里 里 里 尾部 尾部 尾部 避免 使用 两 个 或 或 以上 的 的 自身 自身 递归 调用 调用 和 其它 和 下 下 一 个 绑定 的 数据. 只 只 在 尾部 尾部 调用 自己 自己 自己 后 后 后 后 后 后 后 后 返回 返回 返回 绑定 函数 函数 函数 函数 函数 函数 函数 函数 函数 的 的 的 的 自身 自身 递归 递归 调用 和 和 其它 和 和 下 一 个 个 函数 函数 函数 函数 绑定 绑定 绑定 的 的 的 自身 自身 递归 递归 调用 和 和 其它 和 和 下 一 个 个 函数 函数 函数 函数 绑定 绑定 的 的 的 的 自身 自身 递归 调用 调用 和 和 其它 和 和 下는지是完成状态,这样才会使其及时脱离栈.
即把计算结果当作参数而不是返回值.
需要注意的是, ES6에서 标准里, js才能支持尾递归,且必须에서 严格模式下才行!
"use strict";
// tail calls
function PTCCountVowel(count, sentence) {
count += isVowel(sentence[0]) ? 1 : 0;
if(sentence.length < 2) return count;
return PTCCountVowel(count, sentence.slice(1))
}
console.log(PTCCountVowel(
0,
"Create and share beautiful images of your source code."
)//22
由于第一个参数
count
在每次调用时都应该是0,所以可以使用柯里化省去这一参数:"use strict";
function PTC(count) {
return function inner(sentence) {
count += isVowel(sentence[0]) ? 1 : 0;
if(sentence.length < 2) return count;
return inner(sentence.slice(1))
}
}
const PTCCountVowel = PTC(0);
PTCCountVowel(
"Create and share beautiful images of your source code."
)//22
CPS
如果不依赖尾调用,那么还有其它方式避免栈溢出,这里介绍两种方式.
其一就是CPS(Continuation-Passing Style),这种方法比较难理解,也难写,效率也不高,并且可能会有堆内存存储问题.并不建议使用此方法.
代码例子:
"use strict";
function CPS(sentence, count = v => v) {
let first = isVowel(sentence[0]) ? 1 : 0;
if(sentence.length < 2) return count(first);
return CPS(sentence.slice(1), function f(v) {
return count(first + v);
})
}
CPS(
"Create and share beautiful images of your source code."
)//22
트램폴린
第二种方法是
Trampolines
,单词的意思是蹦床,描述为栈里的函数像是蹦床一样进去一个,出来一个,一上一下,避免都堆在栈里.function trampoline(fn) {
return function trampolined(...args) {
let result = fn(...args);
while (typeof result == "function") {
result = result();
}
return result;
}
}
const countVowel = trampoline(function f(count, sentence){
count += isVowel(sentence[0]) ? 1 : 0;
if(sentence.length < 2) return count;
return function fn() {
return f(count, sentence.slice(1))
}
})
countVowel(
0,
"Create and share beautiful images of your source code."
)//22
Reference
이 문제에 관하여(函数式编程之递归的几种方式), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/kartjim/han-shu-shi-bian-cheng-zhi-di-gui-de-ji-chong-fang-shi-32m텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)