Haskell中的尾递归二项式系数函数

时间:2015-03-06 10:15:32

标签: 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

是否可以修改它并使其尾递归?

3 个答案:

答案 0 :(得分:10)

是。有一个使用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,另一个解决方案是在每一步(AFAIK Rational在幕后进行)将gcd除以它们。

答案 1 :(得分:3)

是的,如果你引入一个带有额外参数的辅助函数,它是可能的:

-- 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操作数被截断并与原始版本相反,没有数学证明要除的值总是为分母的倍数。所以这个功能必须由PetrPudlá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实现中,如果为nk选择较高的值,那么“正确的尾递归”变体仍会吞噬大量内存,您可能会感到失望,因为您正在进行交易堆空间在非尾递归实现中堆栈空间,因为haskell太懒,无法及时计算所有产品。它一直等到你真的需要这个值(可能是打印它),然后只在堆上存储两个产品表达式的表示。为避免这种情况,您应该在第一个和第二个参数中使用binom_and_multiply,因为他们会说 strict,因此在进行尾递归时会急切地评估产品。例如,可以将numdenom与零进行比较,这需要在继续之前评估因子的表达式:

-- 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_and_multiply的递归调用可能仅在new_numnew_denom被评估之后发生(对WHNF,但解释WHNF超出范围)这个问题)。

最后一句话:这个答案通常被称为将右侧折叠转换为左侧折叠然后制作左侧折叠

答案 2 :(得分:0)

"自动"使函数尾递归的方法是使用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添加到其monadic堆栈中,就可以将许多monadic函数转换为尾递归函数。