Haskell中Double类型连续除法的精度问题求解
在Haskell中避免Double除以2的精度损失(不使用Data.Scientific)
问题背景
我在Haskell中尝试将Double类型浮点数连续除以2.0达一百次,当计算到值5960464477539.06250时,除以2.0得到的结果为2.9802322387695313e12(即2980232238769.53130),但数学上的正确结果应为2980232238769.53125,存在明显精度损失。
测试代码:
import Text.Printf (printf) import System.Exit (exitSuccess) main = do n <- readLn :: IO Double reading n 0 reading n c = do if c == 100 then exitSuccess else printf "%.5f\n" n reading (n/2.0) (c+1)
GHCI直接计算验证:
Prelude> 5960464477539.06250/2 2.9802322387695313e12
解决方案(无需额外模块)
方法1:用整数+幂次计数手动模拟
因为所有操作都是连续除以2,我们可以将浮点数转换为「整数 + 2的幂次分母」的形式,通过调整整数或幂次计数来完成计算,完全避免浮点数精度损失:
import Text.Printf (printf) import System.Exit (exitSuccess) -- 用整数num和幂次count表示数值:value = num / (2^count) readingInt :: Integer -> Int -> IO () readingInt num count = do if count == 100 then exitSuccess else do let current = fromIntegral num / (2 ^ count) printf "%.5f\n" current -- 优先将整数除以2(偶数时),否则增加幂次计数 if even num then readingInt (num `div` 2) count else readingInt num (count + 1) main = do n <- readLn :: IO Double -- 将输入Double转换为整数+2的幂次形式 let (num, k) = findIntegerRep n 0 readingInt num k where findIntegerRep x k | x == fromIntegral (round x) = (round x, k) | otherwise = findIntegerRep (x * 2) (k + 1)
原理说明
findIntegerRep会找到最小的整数k,使得输入值乘以2^k后是整数,从而将原数表示为num / 2^k的精确形式- 每次除以2时,若当前整数是偶数,直接对整数做整除操作;否则增加幂次计数,全程无精度损失
- 输出时再转换回Double,或可直接输出分数形式以保留绝对精度
方法2:使用标准库的Rational类型
Haskell标准库Data.Ratio提供了精确的分数类型Rational,它用整数对存储分子和分母,所有运算都是精确的,无需额外安装模块:
import Text.Printf (printf) import System.Exit (exitSuccess) import Data.Ratio ((%)) main = do n <- readLn :: IO Double -- 将Double转换为Rational(有限小数可精确转换) let r = toRational n readingRational r 0 readingRational :: Rational -> Int -> IO () readingRational r c = do if c == 100 then exitSuccess else do let current = fromRational r :: Double printf "%.5f\n" current -- 除以2等价于乘以1/2 readingRational (r * (1%2)) (c+1)
原理说明
Rational会精确存储输入的有限小数(如x.0625这类可表示为2的负幂次的数)- 每次除以2就是将分数乘以
1/2,运算过程完全精确 - 输出时转换回Double即可,若需要更高精度的输出,可直接格式化分数字符串
精度损失的原因
Double是基于IEEE 754标准的64位浮点数,其尾数仅能存储53位有效二进制数字。当数值大到一定程度时,无法精确表示所有小数部分——比如2980232238769.53125的二进制表示所需位数超过了Double的尾数容量,因此会被舍入到最近的可表示值2980232238769.5313。
内容的提问来源于stack exchange,提问作者eightShirt
相关产品推荐
相关产品推荐

