如何高效提取并化简小数的分子与分母?
问题修复与优化方案
核心问题分析
你提供的代码处理0.05得到0/1,主要由三个致命错误导致:
- 指数位计算错误:未对双精度浮点数的11位指数位做掩码处理,导致指数值完全失真,最终分子溢出为0。
- Normalize方法逻辑混乱:错误的
numerator += denominator;和numerator %= denominator;直接破坏分子原始值,将其转为余数。 - 亚正规数处理缺失:未区分正规数与亚正规数的尾数结构,亚正规数尾数无隐含最高位1,直接拼接会导致数值错误。
此外,仅支持double类型会因二进制浮点数的局限性导致精度损失,比如0.05无法用double精确存储,只能得到近似值。
修复后的完整代码
using System.Numerics; // .NET Core 3.0+ 支持BitOperations,旧框架可替换为自定义方法 public class Fraction { public long Numerator { get; private set; } public long Denominator { get; private set; } public Fraction(double chance) { long num = 0; long den = 1; unchecked { ulong bitwiseRepr = BitConverter.DoubleToUInt64Bits(chance); ulong signBit = bitwiseRepr >> 63; ulong expBits = (bitwiseRepr >> 52) & 0x7FF; // 掩码提取11位指数位 ulong mntsBits = bitwiseRepr & 0x000FFFFFFFFFFFFF; // 精确获取低52位尾数 long signFactor = signBit == 1 ? -1 : 1; if (expBits == 0x7FF) // 处理无穷大与NaN { num = mntsBits != 0 ? long.MaxValue : 0; den = mntsBits != 0 ? 0 : signFactor; goto NormalWay; } ulong significandUInt; int expOffset; if (expBits == 0) // 处理亚正规数 { significandUInt = mntsBits; expOffset = -126 - 52; // 亚正规数指数固定为-126,尾数无隐含1 } else // 处理正规数 { significandUInt = (1UL << 52) | mntsBits; expOffset = (int)expBits - 127 - 52; } if (significandUInt == 0) // 输入为0的情况 { goto NormalWay; } // 无循环计算尾数 trailing zeros int nTrailingZeros = BitOperations.TrailingZeroCount(significandUInt); significandUInt >>= nTrailingZeros; expOffset += nTrailingZeros; long significand = (long)significandUInt; if (expOffset < 0) { num = signFactor * significand; den = 1L << -expOffset; } else { if (expOffset >= 63) throw new OverflowException("数值过大,无法转换为long类型分数"); num = signFactor * significand * (1L << expOffset); den = 1; } } NormalWay: Numerator = num; Denominator = den; Normalize(); } // 支持decimal类型,解决十进制小数精度问题 public Fraction(decimal value) { int[] bits = decimal.GetBits(value); long sign = bits[3] < 0 ? -1 : 1; int exponent = (bits[3] >> 16) & 0x1F; ulong mantissa = ((ulong)bits[2] << 64) | ((ulong)bits[1] << 32) | (uint)bits[0]; if (mantissa > long.MaxValue) throw new OverflowException("decimal尾数过大,无法转换为long类型分数"); Numerator = (long)mantissa * sign; Denominator = (long)Math.Pow(10, exponent); Normalize(); } [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] private void Normalize() { // 将负号移至分子,确保分母为正 if (Denominator < 0) { Numerator *= -1; Denominator *= -1; } if (Numerator == 0) { Denominator = 1; return; } // 计算最大公约数约分 long absNum = Math.Abs(Numerator); long gcd = GCD(absNum, Denominator); if (gcd > 1) { Numerator /= gcd; Denominator /= gcd; } } [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] private static long GCD(long a, long b) { while (b != 0) { long t = b; b = a % b; a = t; } return a; } // 兼容旧.NET框架的trailing zeros计算(替代BitOperations) private static int CountTrailingZeroes(ulong v) { if (v == 0) return 64; int counter = 0; while ((v & 1) == 0) { v >>= 1; counter++; } return counter; } public override string ToString() => $"{Numerator}/{Denominator}"; }
关键优化点说明
- 修复指数位计算:通过
& 0x7FF掩码提取正确的11位指数位,确保指数值准确。 - 重构Normalize方法:移除错误逻辑,仅保留符号处理与最大公约数约分,保证分数化简正确。
- 支持亚正规数:区分正规数与亚正规数的尾数结构,避免数值错误。
- 无循环优化:使用
BitOperations.TrailingZeroCount替代循环计算 trailing zeros(旧框架可替换为CountTrailingZeroes方法)。 - 添加decimal支持:decimal是十进制浮点数,可精确表示0.05这类十进制小数,解决double的精度损失问题。
测试验证
- 使用
new Fraction(0.05d):因double的0.05是近似值,会得到与1/20等价的分数(如3602879701896397/72057594037927940)。 - 使用
new Fraction(0.05m):decimal的0.05是精确值,会直接得到1/20。
内容的提问来源于stack exchange,提问作者BasicallyIAmFox
相关产品推荐
相关产品推荐

