简体   繁体   English

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

[英]Tail recursive binomial coefficient function in Haskell

I have a function that calculates the binomial coefficient in Haskell, it looks like this: 我有一个函数来计算Haskell中的二项式系数,它看起来像这样:

binom :: Int -> Int -> Int
binom n 0 = 1
binom 0 k = 0
binom n k = binom (n-1) (k-1) * n `div` k

Is it possible to modify it and make it tail recursive? 是否可以修改它并使其尾递归?

Yes. 是。 There is a standard trick of using an accumulator to achieve tail recursion . 有一个使用累加器来实现尾递归的标准技巧。 In your case, you'll need two of them (or accumulate one rational number): 在您的情况下,您将需要其中两个(或累积一个有理数):

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)

Update: For large binomial coefficients, better use Integer as Int can easily overflow. 更新:对于大二项式系数,最好使用Integer因为Int很容易溢出。 Moreover in the above simple implementation both numerator and denominator can grow much larger than the final result. 此外,在上述简单实现中,分子和分母都可以比最终结果大得多。 One simple solution is to accumulate Rational , another would be to divide both by their gcd at every step (which AFAIK Rational does behind the scenes). 一个简单的解决方案是积累Rational ,另一个解决方案是在每一步都将它们的gcd分开(AFAIK Rational在幕后进行)。

Yes, it is possible, if you introduce a helper function that takes an extra parameter: 是的,如果你引入一个带有额外参数的辅助函数,它是可能的:

-- 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

The last line could be rewritten in point-free style: 最后一行可以用无点样式重写:

binom = binom_and_multiply 1

EDIT: The function above shows the idea, but is actually broken, because the div operand truncates and opposed to the original version, there is no mathematical proof that the value to divide always is a multiple of the denominator. 编辑:上面的函数显示了这个想法,但实际上是破坏了,因为div操作数截断并与原始版本相反,没有数学证据证明除以的值总是分母的倍数。 So that function must be replaced by the suggestion by Petr Pudlák: 所以这个功能必须由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

In non-optimizing haskell implementations, you might be disappointed that the "properly tail recursive" variant still gobbles up lots of memory if you choose high values for n and k , because you are trading stack space in the non tail-recursive implementation by heap-space, as haskell is too lazy to calculate all the products just in time. 在非优化的haskell实现中,你可能会失望的是,如果为nk选择高值,“正确的尾递归”变量仍然会占用大量内存,因为你在堆的非尾递归实现中交易堆栈空间-space,因为haskell太懒了,无法及时计算所有产品。 It waits until you really need the value (maybe to print it), and just stores a representation of the two product expressions on the heap. 它一直等到你真的需要这个值(可能是打印它),然后只在堆上存储两个产品表达式的表示。 To avoid that, you should make binom_and_multiply as they say strict in the first and second parameter , so the product will be eagerly evaluated while doing the tail recursion. 为了避免这种情况,你应该使用binom_and_multiply因为他们在第一个和第二个参数中严格 ,所以在进行尾递归时会急切地评估产品。 For example, one could compare num and denom to zero, which will need to evaluate the expression for factor before going on: 例如,可以将numdenom比较为零,这需要在继续之前评估因子的表达式:

-- calculate (n choose k) * num `div` denom
binom_and_multiply 0 0 _ _ = undefined  -- can't happen, div by zero
-- remaining expressions go here.

The general way to make sure the product is not "evalutated to large" is using the seq function: 确保产品“未评估为大”的一般方法是使用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)

This tells the haskell implementation that the recursive call to binom_and_multiply may only occur after new_num and new_denom has been evaluted (to WHNF, but explaining WHNF is out-of-scope for this question). 这告诉haskell实现,对binom_and_multiply的递归调用可能只在new_numnew_denomnew_denom之后才会发生(对于WHNF,但解释WHNF超出了这个问题的范围)。

One last remark: What this answer was doing is called generally transforming a right-fold into a left-fold and then making the left-fold strict . 最后一句话:这个答案所做的通常被称为将右折叠转换为左折叠然后使左折叠严格

An "automatic" way to make a function tail recursive is to rewrite it using continuation passing style (which is by definition tail recursive). 使函数尾递归的“自动”方法是使用连续传递样式 (根据定义尾递归)重写它。 Arguably a straightforward way to do it in Haskell is to convert the original function into monadic form and then use Cont monad to execute the result: 可以说,在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

Note: This way many monadic functions can be converted into tail recursive ones simply by adding ContT transformer to their monadic stack. 注意:这样,只需将ContT转换器添加到其monadic堆栈中,就可以将许多monadic函数转换为尾递归函数。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM