小数循环节
【问题】
我们都会手算除法,比如: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
这里首先构造出 商 和 余数 的无限列。
再从列中算出循环节的起始和结束位置。
然后就是简单的拼接了。