haskell 基础题解(30)

小数循环节

【问题】
我们都会手算除法,比如:123除以13。
当然,有的时候会出现无限循环小数。
1 请模拟手算除法的过程,计算a除以b的小数后100位。
2 发现无限循环小数的循环节,并标识出来。
比如:123除以13,表示为:
9.[461538]

这个题目要求我们模拟手算除法的过程。初看,用命令式的思考更直观,但因为要保存处理计算的过程,也并不占便宜。。。。
换用 haskell 呢? 如果觉得有障碍,就是不能改变变量的值了。这可以通过传入传出状态来解决。先模拟命令式的想法:

import Data.List (elemIndex)

----求真分数的十进制小数形式,保证: n分子 < n分母
----返回值 (商的不循环部分,商的循环部分),都是逆序的
f真分数 :: Integral a => a -> a -> ([a],[a])
f真分数 n分子 n分母 = f [] [n分子]
    where 
    f l商 l余 = case (head l余 * 10) `divMod` n分母 of
        (t, 0) -> (t:l商, [])
        (t, u) -> case u `elemIndex` l余 of
            Just n  -> (drop n l商, t:take n l商) 
            Nothing -> f (t:l商) (u:l余)

f分数 :: (Show a, Integral a) => a -> a -> String
f分数 n分子 n分母 = let 
                        (n整数部分, n余数) = n分子 `divMod` n分母 
                        (l商, l商循环) = f真分数 n余数 n分母
                    in  show n整数部分 
                        ++ "." 
                        ++ (concat . map show . reverse $ l商)
                        ++ "["
                        ++ (concat . map show . reverse $ l商循环)
                        ++ "]"

main :: IO ()
main = do 
    putStrLn $ f分数 1 3
    putStrLn $ f分数 123 13
    putStrLn $ f分数 23 56

这里,我们用 l商 l余 分别表示商和余数的序列,并在 f 函数中传递。
递归的出口是:要以余数遇到了 0, 要么余数出现了循环。
之所以都用逆序,是因为 ( : ) 比 ( ++ ) 更有效率而已。
然后再把输出安排一下就好了。

既然是函数式编程,就要跳出命令式的圈子。我们需要的是。。定义。。。定义。。不是步骤。。。不是步骤。。。
如上口诀念了 N 遍之后,得出一解:

import Data.List (elemIndex, splitAt)

--- 把真分数表示为循环小数的形式
--- 1/3    = 0.3333 --> ([],[3])
--- 23/56  = 0.410714285771428... ---> ([4,1,0], [7,1,4,2,8,5])
--- 1/5    = 0.200.... --> ([2],[0]) 能除尽的小数,看作以 [0] 为其小数的循环部分
f真分数循环 :: Integral a => a -> a -> ([a],[a])
f真分数循环 n分子 n分母 = 
    let 
        xs = iterate (\(_,u) -> (u * 10) `divMod` n分母) (0,n分子)
        l商 = tail $ map fst xs --从小数点后记商,甩掉头0
        l余 = map snd xs
        ys  = map (`elemIndex` l余) l余 ---元素在余数列中首次出现位置
        (n节尾, n节首) =  head [(a,b) | (Just a,Just b) <- ys `zip` tail ys, a>=b]
    in
        splitAt n节首 (take (n节尾+1) l商) 

ok :: (Show a, Integral a) => a -> a -> String
ok n分子 n分母 = let 
    (n整部, n零部) = n分子 `divMod` n分母
    (xs,ys) = f真分数循环 n零部 n分母
    in
    show n整部 ++ "." 
        ++ concatMap show xs 
        ++ "[" ++ concatMap show ys ++ "]"

main :: IO ()
main = do 
    putStrLn $ ok 1 3
    putStrLn $ ok 123 13
    putStrLn $ ok 23 56

这里首先构造出 商 和 余数 的无限列。
再从列中算出循环节的起始和结束位置。
然后就是简单的拼接了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值