Haskell中的尾递归二项式系数函数
Tail recursive binomial coefficient function in Haskell
我有一个计算Haskell中的二项式系数的函数,它看起来像这样:
binom :: Int -> Int -> Int
binom n 0 = 1
binom 0 k = 0
binom n k = binom (n-1) (k-1) * n `div` k
是否可以修改它并使其成为尾递归?
是的。有一个使用 an accumulator to achieve tail recursion 的标准技巧。在你的情况下,你需要其中两个(或累积一个有理数):
binom :: Int -> Int -> Int
binom = loop 1 1
where
loop rn rd _ 0 = rn `div` rd
loop _ _ 0 _ = 0
loop rn rd n k = loop (rn * n) (rd * k) (n-1) (k-1)
更新: 对于大的二项式系数,最好使用 Integer
因为 Int
很容易溢出。此外,在上面的简单实现中,分子和分母都可以比最终结果大得多。一个简单的解决方案是累积 Rational
,另一种方法是在每一步都除以它们的 gcd(AFAIK Rational
在幕后进行)。
是的,这是可能的,如果你引入一个带有额外参数的辅助函数:
-- 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
编辑:上面的函数显示了这个想法,但实际上被打破了,因为 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
和 [=17= 选择高值,您可能会感到失望,因为 "properly tail recursive" 变体仍然会占用大量内存],因为你交易的是堆space非尾递归实现中的堆-space,因为haskell懒得及时计算所有产品。它会一直等到您真正需要该值(可能是打印它),然后才将两个乘积表达式的表示形式存储在堆上。为避免这种情况,您应该在第一个和第二个参数 中设置 binom_and_multiply
,正如他们所说的 strict,这样在执行尾递归时会急切地评估产品。例如,可以将 num
和 denom
与零进行比较,这将需要在继续之前计算 factor 的表达式:
-- calculate (n choose k) * num `div` denom
binom_and_multiply 0 0 _ _ = undefined -- can't happen, div by zero
-- remaining expressions go here.
确保产品不是 "evalutated to large" 的一般方法是使用 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_and_multiply
的递归调用可能只发生在 new_num
和 new_denom
被评估之后(对 WHNF,但解释 WHNF 是超出此问题的范围)。
最后一句话:这个答案所做的通常称为将右折转换为左折然后使左折严格.
使函数尾递归的"automatic"方法是使用continuation passing style重写它(根据定义尾递归)。
可以说在 Haskell 中一个直接的方法是将原始函数转换为 monadic 形式,然后使用 Cont monad 执行结果:
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
注意:通过这种方式,只需将 ContT transformer 添加到它们的单子堆栈即可将许多单子函数转换为尾递归函数。
我有一个计算Haskell中的二项式系数的函数,它看起来像这样:
binom :: Int -> Int -> Int
binom n 0 = 1
binom 0 k = 0
binom n k = binom (n-1) (k-1) * n `div` k
是否可以修改它并使其成为尾递归?
是的。有一个使用 an accumulator to achieve tail recursion 的标准技巧。在你的情况下,你需要其中两个(或累积一个有理数):
binom :: Int -> Int -> Int
binom = loop 1 1
where
loop rn rd _ 0 = rn `div` rd
loop _ _ 0 _ = 0
loop rn rd n k = loop (rn * n) (rd * k) (n-1) (k-1)
更新: 对于大的二项式系数,最好使用 Integer
因为 Int
很容易溢出。此外,在上面的简单实现中,分子和分母都可以比最终结果大得多。一个简单的解决方案是累积 Rational
,另一种方法是在每一步都除以它们的 gcd(AFAIK Rational
在幕后进行)。
是的,这是可能的,如果你引入一个带有额外参数的辅助函数:
-- 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
编辑:上面的函数显示了这个想法,但实际上被打破了,因为 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
和 [=17= 选择高值,您可能会感到失望,因为 "properly tail recursive" 变体仍然会占用大量内存],因为你交易的是堆space非尾递归实现中的堆-space,因为haskell懒得及时计算所有产品。它会一直等到您真正需要该值(可能是打印它),然后才将两个乘积表达式的表示形式存储在堆上。为避免这种情况,您应该在第一个和第二个参数 中设置 binom_and_multiply
,正如他们所说的 strict,这样在执行尾递归时会急切地评估产品。例如,可以将 num
和 denom
与零进行比较,这将需要在继续之前计算 factor 的表达式:
-- calculate (n choose k) * num `div` denom
binom_and_multiply 0 0 _ _ = undefined -- can't happen, div by zero
-- remaining expressions go here.
确保产品不是 "evalutated to large" 的一般方法是使用 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_and_multiply
的递归调用可能只发生在 new_num
和 new_denom
被评估之后(对 WHNF,但解释 WHNF 是超出此问题的范围)。
最后一句话:这个答案所做的通常称为将右折转换为左折然后使左折严格.
使函数尾递归的"automatic"方法是使用continuation passing style重写它(根据定义尾递归)。 可以说在 Haskell 中一个直接的方法是将原始函数转换为 monadic 形式,然后使用 Cont monad 执行结果:
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
注意:通过这种方式,只需将 ContT transformer 添加到它们的单子堆栈即可将许多单子函数转换为尾递归函数。