我对this文章有疑问。
在此代码之间
function odds(n, p) {
if(n == 0) {
return 1
} else {
return (n / p) * odds(n - 1, p - 1)
}
}
和此代码
(function(){
var odds1 = function(n, p, acc) {
if(n == 0) {
return acc
} else {
return odds1(n - 1, p - 1, (n / p) * acc)
}
}
odds = function(n, p) {
return odds1(n, p, 1)
}
})()
1)我对这有多大帮助感到困惑。第二个片段是否只是一个尾部调用,它会产生更少的开销,因为它会在它再次调用之前计算它需要的内容,或者是否还有一些我缺少的内容?
据我所知,尾调用仍然没有消除,只是优化了。
2)为什么还需要odds
和odds1
?我现在还不清楚。
答案 0 :(得分:1)
我对这有多大帮助感到困惑。第二个片段是否只是一个尾部调用,它会产生更少的开销,因为它会在它再次调用之前计算它需要的内容,或者是否还有一些我缺少的内容?
据我所知,尾调用仍然没有消除,只是优化了。
如果程序结束如下:
push args
call foo
return
然后编译器可以将其优化为
jump startOfFoo
完全取消程序调用。
为什么还需要赔率和赔率?我现在还不清楚。
odds
的“契约”只指定了两个参数 - 第三个参数只是一个实现细节。因此,您将其隐藏在内部方法中,并将“包装器”作为外部API。
我可以将odds1
称为oddsImpl
,我认为这会更清晰。
答案 1 :(得分:1)
第一个版本不是tail recursive,因为在获得odds(n - 1, p - 1)
的值后,必须将其乘以(n / p)
,第二个版本会将其移动到参数的计算中odds1
函数使尾部递归正确。
如果你看一下调用堆栈,那么第一个就是这样:
odds(2, 3)
odds(1, 2)
odds(0, 1)
return 1
return 1/2 * 1
return 2/3 * 1/2
而第二个是:
odds(2, 3)
odds1(2, 3, 1)
odds1(1, 2, 2/3)
odds1(0, 1, 1/2 * 2/3)
return 1/3
return 1/3
return 1/3
return 1/3
因为您只是返回递归调用的值,编译器可以轻松地优化它:
odds(2, 3)
#discard stackframe
odds1(2, 3, 1)
#discard stackframe
odds1(1, 2, 2/3)
#discard stackframe
odds1(0, 1, 1/3)
return 1/3
odds
和odds1
的原因只是在其他代码调用此函数时提供初始累加器值。
答案 2 :(得分:1)
尾递归的优化如下,在第一个例子中,因为在你调用赔率(n-1)之前你不能计算乘法return (n / p) * odds(n - 1, p - 1)
的结果,所以交织者必须保持我们当前的位置在内存中(在堆栈上),并打开一个新的赔率调用。
递归地,这将在下一个调用中发生,并且在它之后发生,依此类推。因此,当我们到达递归结束并开始返回值并计算产品时,我们有n个待处理的操作。
在第二个例子中,由于执行的return语句只是return odds1(n - 1, p - 1, (n / p) * acc)
,我们可以计算函数参数,只需调用odds1(n-1)而不保持当前位置。这是优化的地方,因为现在我不必记住我每次在堆栈上打开一个新框架时的位置。
把它想象成书籍参考。想象一下你打开一本食谱然后去一个食谱,其成分列举如下:
下一页有
等。你怎么知道所有的成分是什么?你必须记住你在每一页上看到的内容!
虽然第二个例子更像是以下成分列表:
下一页有:
等。当你到达最后一页时(注意类比是准确的,因为两者都需要相同数量的函数调用),你拥有所有的成分,而不必“记住”你在每一页上看到的内容,因为它是最后一页都在那里!