我一直以不好的眼光看待递归函数,我拒绝了它们,转而支持其他选择。
我为什么要这样做?:
foo(0);
function foo(cont){
console.log(cont);
cont++;
if(cont<50){
foo(cont);
}
}
能够做到这一点:
foo();
function foo(){
for(let cont=0;cont<50;cont++){
console.log(cont);
}
}
查了资料,发现可以使用递归函数来优化Tail Call Optimization
递归函数式javascript
如果在前面的片段中我们模拟一个更大的循环,我觉得没有多大意义,例如:cont < 99999
. 当他完成它时,它función recursiva
在到达终点之前失败了。for
另一方面,如果我们模拟一个无限循环,for
则不会这样做,而función recursiva
确实会尝试并再次给出错误。
说这是不好的做法是夸大其词,但如果你能避免它,那就更好了。
我解释,
使用递归函数可以更快地填充调用堆栈,并且很可能会抛出
Maximum call stack size exceeded
未经检查的错误。递归函数的另一个缺点是它们会使调用堆栈非常长,这会使调试变得困难,因为您必须搜索所有迭代失败的记录,这对解决问题没有多大帮助。这就是为什么递归函数的使用必须限于不是很长的递归。
最大堆栈大小是特定于浏览器的。例如 chrome 在运行 32k 时可以接受 21k。
为了避免这种情况,有循环
while
,for
并且foreach
它们不会将迭代执行的每一行代码保存在调用堆栈中,因此它们没有执行限制,因为它们不会填充堆栈,并且它们也很容易识别错误因为堆栈非常小。与递归相比。更新1:
如果你有兴趣减少堆栈的大小,你可以说是。如果可以避免递归,那就更好了。
循环可以像递归一样解决问题,而且成本更低,因此循环比递归更好。
setInterval
不是循环,而是将函数引用保存在一个任务中,该任务在主 javascript 循环可用时执行并且不会添加到堆栈上,因为最后使用了相同的函数引用。TL;博士
这不是坏习惯,它是一种资源。
问题真的应该是是否应用递归并将上下文缩小到特定问题。
当一个问题可以被建模为一个调用自身的函数并且每次调用都更接近于一个截止条件(它停止调用自身,返回一个结果)时,一个问题适合用递归解决。的)解决方案。
功能状态
在您比较的两种情况下,有一个根本区别:函数的状态。
在递归的情况下,函数的所有变量(状态)在迭代时都会被保留。这样每次通话结束时,它会返回一个步骤,并自动恢复状态。
在循环(
for
/while
)的情况下,状态随着每次迭代而变化,并且如果您想保留当前状态(当时变量的值)以供将来返回(回溯),则必须使用一些辅助结构,例如充当堆栈的向量,并在其中放置带有我们需要的变量的对象。所以我们以某种方式最终以一种更复杂的方式做与递归算法相同的事情。公开的示例非常简单,但在这种情况下,状态由变量 表示,
count
除了知道何时剪切之外,没有太多理由在本练习中记住该状态。要解决其他问题,例如评估棋盘上的移动(例如,可以左右移动的象),保留每个循环或迭代中的状态很重要。
还解决逻辑问题,其中测试了所有替代方案(有限或高达 x 级别的调用深度),直到找到一个解决问题的方案(或确定它没有解决方案)。
虽然这个问题仅限于 javascript,但其他语言如 Prolog 都将递归作为语言的核心部分。
出于这个原因,递归是适合或不适合给定问题的资源,而不是一般的好或坏实践。
示例(船夫问题)
举个例子,我将使用一个已知问题:
"牧羊人必须使用船/驳船从河流的一侧穿过狼、山羊和莴苣植物到另一侧,只有牧羊人和三个要素之一(狼、山羊或莴苣)才能穿过进入。)。问题是,出于显而易见的原因,狼不能与山羊单独相处,山羊与生菜也不能单独相处。牧羊人如何跨越三个元素?
提示:牧羊人可以单独在船上过河。
为了解决这个练习,我们需要:
1)对问题状态建模:
它可以为一个包含 4 个元素(WOLF、GOAT、LETTUCE、SHEPHERD)的数组提供服务,其中每个元素在河的左岸可以为真,如果在河的右岸则为假。
2)确定一个初始状态,也就是这个数组中所有的真值(都在左边距)。
3) 确定一个最终状态,也就是所有假值的同一个数组(都已成功越过右边距)。
4)从初始状态开始,迭代所有可能的交叉点,直到达到最终状态(解决方案),或者直到没有可能的交叉点可以执行。可能的穿越是那些要穿越的元素与牧羊人在同一侧,穿越后任何人都不能吃任何人的穿越。
注意:在这个问题中可能会出现循环(狼和牧羊人不断地从河流的一侧到另一侧来回走动),因此您必须有额外的控制来检测这些循环并打破它们。
重要的是要注意,至少对于这些解决方案,必须“记住”以前的状态,从而导致每个新状态。这是因为在尝试了给定状态的所有可能结点之后,如果它们都没有达到解决方案,则必须丢弃该状态,并且必须再次尝试先前的状态。
在使用递归创建的程序中,状态和路径与正在执行的行和
for
指示被越过的元素的下标值一起保留在程序的调用堆栈中。在使用
while
状态创建的程序中,与路径和正在交叉的元素一起,它们被保存在一个向量中,该向量在同一程序中作为堆栈 (LIFO) 工作。程序行在此版本中无关紧要,因为所有内容都在同一个循环中执行,并且每次迭代中要评估的状态都会发生变化,要么生成新状态(并存储当前状态),要么恢复先前存储在向量中的状态。递归提出的程序:
它
for(let i=0; i<4; i++){
尝试一次遍历每个元素,并且每次遍历都会生成一个新的修改状态,该状态用于调用递归函数。发生这种情况时,调用之前的本地值(状态、路径和 的索引
for
)会保留在调用堆栈上,而您无需执行任何其他操作。如果事实证明调用没有导致解决方案,则函数在某个时刻返回到该点,并且状态、路径和索引的值
for
会自动恢复,甚至函数从for
on the内部返回递归调用之后的行。建议的带有循环的解决方案:( 代码可能会有所改进)
在这个版本中,整个程序循环运行。
quienActual
表示要在迭代中交叉的元素。那些能够交叉的元素将生成新的状态进行测试。与以前的版本不同,每次迭代都会替换当前状态。更换可能是由于:产生新状态的交叉口。
push()
在这种情况下,前一个状态通过替换前保存在数组(堆栈)中。一条死路,通过执行 a
pop()
并推进quienActual
以尝试与下一个项目连接来恢复先前的状态。完整代码: