Haskell中的尾部递归二项式系数函数
我有一个计算Haskell中二项式系数的函数,它如下所示:Haskell中的尾部递归二项式系数函数,haskell,recursion,tail-recursion,binomial-coefficients,Haskell,Recursion,Tail Recursion,Binomial Coefficients,我有一个计算Haskell中二项式系数的函数,它如下所示: binom :: Int -> Int -> Int binom n 0 = 1 binom 0 k = 0 binom n k = binom (n-1) (k-1) * n `div` k 是否可以修改它并使其尾部递归?可以。有一个标准的使用技巧。在您的情况下,您将需要其中两个(或累积一个有理数): 更新:对于较大的二项式系数,最好使用Integer,因为Int很容易溢出。此外,在上述简单的实现中,分子和分母都可以比最
binom :: Int -> Int -> Int
binom n 0 = 1
binom 0 k = 0
binom n k = binom (n-1) (k-1) * n `div` k
是否可以修改它并使其尾部递归?可以。有一个标准的使用技巧。在您的情况下,您将需要其中两个(或累积一个有理数):
更新:对于较大的二项式系数,最好使用
Integer
,因为Int
很容易溢出。此外,在上述简单的实现中,分子和分母都可以比最终结果增长得更大。一个简单的解决方案是累加Rational
,另一个方法是在每一步都将两者除以它们的值(AFAIKRational
在幕后就是这样做的)。是的,如果引入一个带有额外参数的辅助函数:
-- calculate factor*(n choose k)
binom_and_multiply factor n 0 = factor
binom_and_multiply factor 0 k = 0
binom_and_multiply factor n k = binom (n-1) (k-1) (factor * n `div` k)
binom n k = binom_and_multiply 1 n k
最后一行可以用无点风格重写:
binom = binom_and_multiply 1
EDIT:上面的函数显示了这个想法,但实际上被破坏了,因为
div
操作数截断并与原始版本相反,没有数学证明要除法的值始终是分母的倍数。因此,这一职能必须由Petr Pudlák的建议取代:
-- calculate (n choose k) * num `div` denom
binom_and_multiply num denom _ 0 = num `div` denom
binom_and_multiply _ _ 0 _ = 0
binom_and_multiply num denom n k = binom_and_multiply num denom (num * n) (denom * k) (n-1) (k-1)
binom = binom_and_multiply 1 1
在非优化haskell实现中,如果为
n
和k
选择较高的值,您可能会对“正确尾部递归”变量仍然占用大量内存感到失望,因为您在非尾部递归实现中用堆空间交换堆栈空间,因为haskell太懒了,无法及时计算所有产品。它会一直等到您真正需要该值(可能需要打印它),然后在堆上存储两个产品表达式的表示形式。为了避免这种情况,您应该按照第一个和第二个参数所说的严格要求,使binom_和_相乘
,这样在进行尾部递归时,产品将被急切地求值。例如,可以将num
和denom
比较为零,这将需要在继续之前评估因子的表达式:
-- calculate (n choose k) * num `div` denom
binom_and_multiply 0 0 _ _ = undefined -- can't happen, div by zero
-- remaining expressions go here.
确保产品不会“蒸发到大”的一般方法是使用seq
功能:
-- calculate (n choose k) * num `div` denom
binom_and_multiply num denom _ 0 = num `div` denom
binom_and_multiply _ _ 0 _ = 0
binom_and_multiply num denom n k =
new_num = num*n
new_denom = denom*k
in new_num `seq` new_denom `seq` binom_and_multiply new_num new_denom (n-1) (k-1)
这告诉haskell实现,对binom\u和\u multiply
的递归调用只能在对new\u num
和new\u denom
进行了求值之后(对WHNF,但解释WHNF超出了这个问题的范围)才会发生
最后一句话:这个答案通常被称为将右折叠转换为左折叠,然后将左折叠严格化。使函数尾部递归的“自动”方法是使用(定义为尾部递归)重写它。
在Haskell中,一种简单易行的方法是将原始函数转换为一元形式,然后使用执行结果:
import Control.Monad.Cont
-- | Original function in monadic form
binomM n 0 = return 1
binomM 0 k = return 0
binomM n k = do
b1 <- binomM (n-1) (k-1)
return $! b1 * n `div` k
-- | Tail recursive mode of execution
binom :: Int -> Int -> Int
binom n k = binomM n k `runCont` id
import Control.Monad.Cont
--|一元形式的原始函数
binomM n 0=返回1
binomm0k=return0
binomM n k=do
b1整数->整数
binom n k=binomM n k`runCont`id
注意:通过这种方式,许多一元函数只需添加到它们的一元堆栈中,就可以转换为尾部递归函数
import Control.Monad.Cont
-- | Original function in monadic form
binomM n 0 = return 1
binomM 0 k = return 0
binomM n k = do
b1 <- binomM (n-1) (k-1)
return $! b1 * n `div` k
-- | Tail recursive mode of execution
binom :: Int -> Int -> Int
binom n k = binomM n k `runCont` id