# 递归算法
程序调用自身称为递归( recursion)。普通递归容易造成 栈溢出
# 阶乘例子
//普通递归
function fac(n) {
if (n === 1) return 1;
return n * fac(n - 1);
}
fac(5) // 120
解析
这是个阶乘。但是占用内存,因为:
fac(5)
(5*fac(4))
(5*(4*fac(3)))
(5*(4*(3*fac(2))))
(5*(4*(3*(2*fac(1)))))
(5*(4*(3*2)))
(5*(4*(6)))
(5*24)
//120
- 函数调用会产生“调用记录(存储着函数的相关信息)”存放在栈中,当有函数返回,对应的调用记录才会消失, 上述用普通递归实现的阶乘的执行过程中,不断的调用自身,导致一直没有返回,这样也就不断的在栈中存储调用记录
- 而当调用自身的次数过多后,就会产生我们常说的“栈溢出”拟人描述: 就想一个人不断地借钱(调用自身,不断向栈中存调用记录),但是总想着以后再还(一直没有返回), 当外债积累到超出自己偿还能力的时候,就跑路了(栈溢出)
//尾递归
function fac(n, total) {
if (n === 1) return total;
return fac(n - 1, n * total);
}
fac(5, 1) // 120
解析
执行过程如下:
fac(5,1)
fac(4,5)
fac(3,20)
fac(2,60)
fac(1,120)
永远只有一个调用记录,调用函数产生一个调用记录,最后一步操作 return fac(n - 1, n * total),把当前函数的计算结果当做参数传递给了下一个自身调用,这样第一个函数调用产生的调用记录就消失了,因为它执行完了,依次类推,就不会溢出
尾递归:函数的最后一步是执行一个函数
尾递归其实并不是自身的优化? (opens new window)
# 用js递归的方式写1到100求和
function add(num1,num2){
var num = num1+num2;
if(num2+1>100){
return num;
}else{
return add(num,num2+1)
}
}
var sum =add(1,2);
# 斐波那契数列与递归和迭代
//普通递归
function feibo(n){
if(n==1||n==2){
return 1
}else{
return feibo(n-1)+feibo(n-2)
}
}
feibo(6); //8
//尾调法
function fb2(n,res1 =1,res2 = 1){
if(n <= 2){
return res2;
}else{
return fb2(n-1,res2,res1 + res2);
}
}
//迭代法
function fb3(n){
var res1 = 1;
var res2 = 1;
var sum = res2;
for(var i = 2;i < n;i ++){
sum = res1 + res2;
res1 = res2;
res2 = sum;
}
return sum;
}
# 使用generator实现斐波那契数列
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
for (let n of fibonacci()) {
if (n > 100000) break;
console.log(n);
}
# 递归函数被重命名解决方案
可以使用arguments.callee在内侧调用函数,但是如果是严格模式则无效,需要创建一个函数来解决
const factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
# 尾调用
尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
以下两种情况,都不属于尾调用。
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
上面代码中,情况一是调用函数g之后,还有别的操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。
尾调用不一定出现在函数尾部,只要是最后一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
- 调用栈:简单地说,调用栈(Call Stack)是一种记录函数地址的栈,当调用一个函数 时,会在调用栈的顶端添加一个新的栈帧(Stack Frame),即入栈;而当函数执行完毕后,就 会将其移出调用栈,即出栈。
在正常情况下,调用栈能够保持有进有出的平衡,但当函数的调用次数激增时(如过多 的递归),调用栈就会消耗大量内存,甚至占满整个内存空间,从而导致栈溢出(Stack Overflow)。不过值得庆幸的是,现代浏览器都不会因为栈溢出而崩溃,其内部已做了限制, 例如,Chrome 仅仅会给出“RangeError: Maximum call stack size exceeded”的错误警告。
- 尾调用优化:为了控制栈帧的数量,减少内存空间的使用,引入了尾调用优化(Tail Call Optimization,TCO)。这也不是一个新语法,只是一种空间上的优化,并且所有的工作都由 JavaScript 引擎(如 V8、SpiderMonkey 等)代劳了。
ES6 中的 TCO 只有在严格模式下才能开启,它的原理就是不需要记住尾调用所处函数的 地址。下面是一个检测字符串回文的函数,当开启 TCO 时,由于函数内需要的值已通过参数 的形式传递进来(即不依赖函数外部的变量),因此每次递归都会覆盖前一次函数调用的栈帧, 即调用栈的数量会保持恒定。
"use strict";
function palindrome(str) {
if (str.length <= 1)
return true;
if (str[0] != str[str.length - 1])
return false;
return palindrome(str.substr(1, str.length - 2));
}
虽然 TCO 的表现很抢眼,但很遗憾,目前除了 Safrai 之外,大部分浏览器暂不支持,因 此在这些浏览器中还享受不到 TCO 带来的性能提升。
# 逆向递归:从树底部选取父级id
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
dds
</body>
</html>
<script type="text/javascript">
const getTreeIds = (tree, nodeId, config) => {
const { children = 'children', id = 'id' } = config || {}
const toFlatArray = (tree, parentId) => {
return tree.reduce((t, _) => {
const child = _[children]
return [
...t,
parentId ? { ..._, parentId } : _,
...(child && child.length ? toFlatArray(child, _[id]) : [])]
}, [])
}
const getIds = flatArray => {
let ids = [nodeId]
let child = flatArray.find(_ => _[id] === nodeId)
while (child && child.parentId) {
ids = [child.parentId, ...ids]
child = flatArray.find(_ => _[id] === child.parentId)
}
return ids
}
return getIds(toFlatArray(tree))
}
const treeData = [
{
id: 1,
label: 'test1',
children: [
{
id: 2,
label: 'test1-1',
children: [
{
id: 3,
label: 'test1-1-1'
},
{
id: 4,
label: 'test1-1-2',
children: [
{
id: 5,
label: 'test1-1-1-1'
}
]
}
]
}
]
}
]
console.log(getTreeIds(treeData, 5)) // 输出 [1, 2, 4, 5]
console.log(getTreeIds(treeData, 3)) // 输出 [1, 2, 3]
</script>
# for 循环和递归结合
public getPreval(jsonPath: string, arr: any[]): any | undefined {
for (let i = 0; i < arr.length; i++) {
if (arr[i].jsonPath == jsonPath) {
return arr[i]?.value
} else if (arr[i].children && arr[i].children.length > 0) {
const result = this.getPreval(jsonPath, arr[i].children)
if (result !== undefined) {
return result
}
}
}
return undefined
}
← 浏览器 valueOf和toString →