将小数化简为分数的算法

Posted

技术标签:

【中文标题】将小数化简为分数的算法【英文标题】:Algorithm for simplifying decimal to fractions 【发布时间】:2011-07-04 17:29:57 【问题描述】:

我尝试编写一个算法来将小数化简为分数,并意识到它并不太简单。

例如,将0.333333... 写成1/3

0.1666667,即1/6

令人惊讶的是,我在网上查看了所有代码,发现的代码要么太长,要么在某些情况下不起作用。更烦人的是它们不适用于循环小数。然而,我想知道这里是否会有一位数学家/程序员了解将小数化为分数的所有相关过程。有人吗?

【问题讨论】:

我有类似的东西,但我只做分数的一个子集。 1/16 至 15/16。我不允许 1/324 或任何会产生 5+ 小数位的东西。我只是制作了一个 Dictionary 并将每个可能的牵引力添加为一个键,它是十进制等效的。然后使用 linq 在字典中搜索十进制值最接近输入十进制值的键。同样,它仅适用于一小部分分数。当你到达百位时,它会失去准确性。 @ChibuezeOpata 刚刚在这里回答了非常相似的问题我的O(1) 解决方案Decimals to Fractions Conversion exploting binary representation of floating point variables 没有循环没有乘法或除法... 【参考方案1】:

其他人给你的算法通过计算数字的Continued Fraction得到答案。这给出了一个保证非常非常快速地收敛的分数序列。但是,保证给您在实数距离 epsilon 内的最小分数。要找到你必须走Stern-Brocot tree。

要做到这一点,你从地板上减去 [0, 1) 范围内的数字,然后你的下估计是 0,你的上估计是 1。现在做一个二分搜索,直到你足够接近。在每次迭代中,如果您的下限是 a/b,而您的上限是 c/d,那么您的中间是 (a+c)/(b+d)。用 x 测试你的中间,或者让中间为上、下,或者返回你的最终答案。

这里有一些非常不惯用的(因此,希望即使您不懂语言也能阅读)Python,它实现了这个算法。

def float_to_fraction (x, error=0.000001):
    n = int(math.floor(x))
    x -= n
    if x < error:
        return (n, 1)
    elif 1 - error < x:
        return (n+1, 1)

    # The lower fraction is 0/1
    lower_n = 0
    lower_d = 1
    # The upper fraction is 1/1
    upper_n = 1
    upper_d = 1
    while True:
        # The middle fraction is (lower_n + upper_n) / (lower_d + upper_d)
        middle_n = lower_n + upper_n
        middle_d = lower_d + upper_d
        # If x + error < middle
        if middle_d * (x + error) < middle_n:
            # middle is our new upper
            upper_n = middle_n
            upper_d = middle_d
        # Else If middle < x - error
        elif middle_n < (x - error) * middle_d:
            # middle is our new lower
            lower_n = middle_n
            lower_d = middle_d
        # Else middle is our best fraction
        else:
            return (n * middle_d + middle_n, middle_d)

【讨论】:

+1 - 这是寻找平滑、人性化分数的绝佳解决方案。 将其翻译成 C# 并添加了该算法的测试结果 -- see my answer 我想出了另一个,显然更快,solution @PinkFloyd 我的解决方案已经指出,有更快的方法来做到这一点。但是它会找到最小的。例如,考虑将 pi 逼近到 0.001 以内。我的会找到 201/64,而你的会找到 333/106。你的会更快,并且是更好的近似值,但我的是最小的,符合所选标准。 +1 确实是一个很好的解决方案,尽管如果您想以 fration 格式显示它,请将最后的 else 部分更改为:else: frac = Fraction(n * middle_d + middle_n, middle_d) if (frac.numerator // frac.denominator) == 0: return(f"frac.numerator % frac.denominator/frac.denominator") elif ((frac.numerator % frac.denominator)/frac.denominator) == 0/1: return(f"frac.numerator // frac.denominator") else: return(f"frac.numerator // frac.denominator "f"frac.numerator % frac.denominator/frac.denominator")【参考方案2】:

(代码在 2017 年 2 月改进 - 向下滚动到“优化”...)

(本答案末尾的算法对比表)

我在 C# 中实现了btilly's answer 和...

增加了对负数的支持 提供accuracy 参数来指定最大值。相对误差,而不是最大值。绝对误差; 0.01 会找到该值 1% 以内的分数。 提供优化 不支持Double.NaNDouble.Infinity;您可能想要处理这些 (example here)。
public Fraction RealToFraction(double value, double accuracy)

    if (accuracy <= 0.0 || accuracy >= 1.0)
    
        throw new ArgumentOutOfRangeException("accuracy", "Must be > 0 and < 1.");
    

    int sign = Math.Sign(value);

    if (sign == -1)
    
        value = Math.Abs(value);
    

    // Accuracy is the maximum relative error; convert to absolute maxError
    double maxError = sign == 0 ? accuracy : value * accuracy;

    int n = (int) Math.Floor(value);
    value -= n;

    if (value < maxError)
    
        return new Fraction(sign * n, 1);
    

    if (1 - maxError < value)
    
        return new Fraction(sign * (n + 1), 1);
    

    // The lower fraction is 0/1
    int lower_n = 0;
    int lower_d = 1;

    // The upper fraction is 1/1
    int upper_n = 1;
    int upper_d = 1;

    while (true)
    
        // The middle fraction is (lower_n + upper_n) / (lower_d + upper_d)
        int middle_n = lower_n + upper_n;
        int middle_d = lower_d + upper_d;

        if (middle_d * (value + maxError) < middle_n)
        
            // real + error < middle : middle is our new upper
            upper_n = middle_n;
            upper_d = middle_d;
        
        else if (middle_n < (value - maxError) * middle_d)
        
            // middle < real - error : middle is our new lower
            lower_n = middle_n;
            lower_d = middle_d;
        
        else
        
            // Middle is our best fraction
            return new Fraction((n * middle_d + middle_n) * sign, middle_d);
        
    

Fraction 类型只是一个简单的结构。当然,使用您自己喜欢的类型...(我喜欢 Rick Davin 的 this one。)

public struct Fraction

    public Fraction(int n, int d)
    
        N = n;
        D = d;
    

    public int N  get; private set; 
    public int D  get; private set; 

2017 年 2 月优化

对于某些值,例如0.010.001 等,算法会经历数百或数千次线性迭代。为了解决这个问题,我实现了一种查找最终值的二进制方法——感谢btilly 的这个想法。在 if-statement 中替换以下内容:

// real + error < middle : middle is our new upper
Seek(ref upper_n, ref upper_d, lower_n, lower_d, (un, ud) => (lower_d + ud) * (value + maxError) < (lower_n + un));

// middle < real - error : middle is our new lower
Seek(ref lower_n, ref lower_d, upper_n, upper_d, (ln, ld) => (ln + upper_n) < (value - maxError) * (ld + upper_d));

这是Seek方法的实现:

/// <summary>
/// Binary seek for the value where f() becomes false.
/// </summary>
void Seek(ref int a, ref int b, int ainc, int binc, Func<int, int, bool> f)

    a += ainc;
    b += binc;

    if (f(a, b))
    
        int weight = 1;

        do
        
            weight *= 2;
            a += ainc * weight;
            b += binc * weight;
        
        while (f(a, b));

        do
        
            weight /= 2;

            int adec = ainc * weight;
            int bdec = binc * weight;

            if (!f(a - adec, b - bdec))
            
                a -= adec;
                b -= bdec;
            
        
        while (weight > 1);
    

算法对照表

您可能希望将表格复制到文本编辑器以进行全屏查看。

Accuracy: 1.0E-3      | Stern-Brocot                             OPTIMIZED   | Eppstein                                 | Richards                                 
Input                 | Result           Error       Iterations  Iterations  | Result           Error       Iterations  | Result           Error       Iterations  
======================| =====================================================| =========================================| =========================================
   0                  |       0/1 (zero)   0         0           0           |       0/1 (zero)   0         0           |       0/1 (zero)   0         0           
   1                  |       1/1          0         0           0           |    1001/1000      1.0E-3     1           |       1/1          0         0           
   3                  |       3/1          0         0           0           |    1003/334       1.0E-3     1           |       3/1          0         0           
  -1                  |      -1/1          0         0           0           |   -1001/1000      1.0E-3     1           |      -1/1          0         0           
  -3                  |      -3/1          0         0           0           |   -1003/334       1.0E-3     1           |      -3/1          0         0           
   0.999999           |       1/1         1.0E-6     0           0           |    1000/1001     -1.0E-3     2           |       1/1         1.0E-6     0           
  -0.999999           |      -1/1         1.0E-6     0           0           |   -1000/1001     -1.0E-3     2           |      -1/1         1.0E-6     0           
   1.000001           |       1/1        -1.0E-6     0           0           |    1001/1000      1.0E-3     1           |       1/1        -1.0E-6     0           
  -1.000001           |      -1/1        -1.0E-6     0           0           |   -1001/1000      1.0E-3     1           |      -1/1        -1.0E-6     0           
   0.50 (1/2)         |       1/2          0         1           1           |     999/1999     -5.0E-4     2           |       1/2          0         1           
   0.33... (1/3)      |       1/3          0         2           2           |     999/2998     -3.3E-4     2           |       1/3          0         1           
   0.67... (2/3)      |       2/3          0         2           2           |     999/1498      3.3E-4     3           |       2/3          0         2           
   0.25 (1/4)         |       1/4          0         3           3           |     999/3997     -2.5E-4     2           |       1/4          0         1           
   0.11... (1/9)      |       1/9          0         8           4           |     999/8992     -1.1E-4     2           |       1/9          0         1           
   0.09... (1/11)     |       1/11         0         10          5           |     999/10990    -9.1E-5     2           |       1/11         0         1           
   0.62... (307/499)  |       8/13        2.5E-4     5           5           |     913/1484     -2.2E-6     8           |       8/13        2.5E-4     5           
   0.14... (33/229)   |      15/104       8.7E-4     20          9           |     974/6759     -4.5E-6     6           |      16/111       2.7E-4     3           
   0.05... (33/683)   |       7/145      -8.4E-4     24          10          |     980/20283     1.5E-6     7           |      10/207      -1.5E-4     4           
   0.18... (100/541)  |      17/92       -3.3E-4     11          10          |     939/5080     -2.0E-6     8           |      17/92       -3.3E-4     4           
   0.06... (33/541)   |       5/82       -3.7E-4     19          8           |     995/16312    -1.9E-6     6           |       5/82       -3.7E-4     4           
   0.1                |       1/10         0         9           5           |     999/9991     -1.0E-4     2           |       1/10         0         1           
   0.2                |       1/5          0         4           3           |     999/4996     -2.0E-4     2           |       1/5          0         1           
   0.3                |       3/10         0         5           5           |     998/3327     -1.0E-4     4           |       3/10         0         3           
   0.4                |       2/5          0         3           3           |     999/2497      2.0E-4     3           |       2/5          0         2           
   0.5                |       1/2          0         1           1           |     999/1999     -5.0E-4     2           |       1/2          0         1           
   0.6                |       3/5          0         3           3           |    1000/1667     -2.0E-4     4           |       3/5          0         3           
   0.7                |       7/10         0         5           5           |     996/1423     -1.0E-4     4           |       7/10         0         3           
   0.8                |       4/5          0         4           3           |     997/1246      2.0E-4     3           |       4/5          0         2           
   0.9                |       9/10         0         9           5           |     998/1109     -1.0E-4     4           |       9/10         0         3           
   0.01               |       1/100        0         99          8           |     999/99901    -1.0E-5     2           |       1/100        0         1           
   0.001              |       1/1000       0         999         11          |     999/999001   -1.0E-6     2           |       1/1000       0         1           
   0.0001             |       1/9991      9.0E-4     9990        15          |     999/9990001  -1.0E-7     2           |       1/10000      0         1           
   1E-05              |       1/99901     9.9E-4     99900       18          |    1000/99999999  1.0E-8     3           |       1/99999     1.0E-5     1           
   0.33333333333      |       1/3         1.0E-11    2           2           |    1000/3001     -3.3E-4     2           |       1/3         1.0E-11    1           
   0.3                |       3/10         0         5           5           |     998/3327     -1.0E-4     4           |       3/10         0         3           
   0.33               |      30/91       -1.0E-3     32          8           |     991/3003      1.0E-5     3           |      33/100        0         2           
   0.333              |     167/502      -9.9E-4     169         11          |    1000/3003      1.0E-6     3           |     333/1000       0         2           
   0.7777             |       7/9         1.0E-4     5           4           |     997/1282     -1.1E-5     4           |       7/9         1.0E-4     3           
   0.101              |      10/99        1.0E-4     18          10          |     919/9099      1.1E-6     5           |      10/99        1.0E-4     3           
   0.10001            |       1/10       -1.0E-4     9           5           |       1/10       -1.0E-4     4           |       1/10       -1.0E-4     2           
   0.100000001        |       1/10       -1.0E-8     9           5           |    1000/9999      1.0E-4     3           |       1/10       -1.0E-8     2           
   0.001001           |       1/999       1.0E-6     998         11          |       1/999       1.0E-6     3           |       1/999       1.0E-6     1           
   0.0010000001       |       1/1000     -1.0E-7     999         11          |    1000/999999    9.0E-7     3           |       1/1000     -1.0E-7     2           
   0.11               |      10/91       -1.0E-3     18          9           |    1000/9091     -1.0E-5     4           |      10/91       -1.0E-3     2           
   0.1111             |       1/9         1.0E-4     8           4           |    1000/9001     -1.1E-5     2           |       1/9         1.0E-4     1           
   0.111111111111     |       1/9         1.0E-12    8           4           |    1000/9001     -1.1E-4     2           |       1/9         1.0E-12    1           
   1                  |       1/1          0         0           0           |    1001/1000      1.0E-3     1           |       1/1          0         0           
  -1                  |      -1/1          0         0           0           |   -1001/1000      1.0E-3     1           |      -1/1          0         0           
  -0.5                |      -1/2          0         1           1           |    -999/1999     -5.0E-4     2           |      -1/2          0         1           
   3.14               |      22/7         9.1E-4     6           4           |     964/307       2.1E-5     3           |      22/7         9.1E-4     1           
   3.1416             |      22/7         4.0E-4     6           4           |     732/233       9.8E-6     3           |      22/7         4.0E-4     1           
   3.14... (pi)       |      22/7         4.0E-4     6           4           |     688/219      -1.3E-5     4           |      22/7         4.0E-4     1           
   0.14               |       7/50         0         13          7           |     995/7107      2.0E-5     3           |       7/50         0         2           
   0.1416             |      15/106      -6.4E-4     21          8           |     869/6137      9.2E-7     5           |      16/113      -5.0E-5     2           
   2.72... (e)        |      68/25        6.3E-4     7           7           |     878/323      -5.7E-6     8           |      87/32        1.7E-4     5           
   0.141592653589793  |      15/106      -5.9E-4     21          8           |     991/6999     -7.0E-6     4           |      15/106      -5.9E-4     2           
  -1.33333333333333   |      -4/3         2.5E-15    2           2           |   -1001/751      -3.3E-4     2           |      -4/3         2.5E-15    1           
  -1.3                |     -13/10         0         5           5           |    -992/763       1.0E-4     3           |     -13/10         0         2           
  -1.33               |     -97/73       -9.3E-4     26          8           |    -935/703       1.1E-5     3           |    -133/100        0         2           
  -1.333              |      -4/3         2.5E-4     2           2           |   -1001/751      -8.3E-5     2           |      -4/3         2.5E-4     1           
  -1.33333337         |      -4/3        -2.7E-8     2           2           |    -999/749       3.3E-4     3           |      -4/3        -2.7E-8     2           
  -1.7                |     -17/10         0         5           5           |    -991/583      -1.0E-4     4           |     -17/10         0         3           
  -1.37               |     -37/27        2.7E-4     7           7           |    -996/727       1.0E-5     7           |     -37/27        2.7E-4     5           
  -1.33337            |      -4/3        -2.7E-5     2           2           |    -999/749       3.1E-4     3           |      -4/3        -2.7E-5     2           
   0.047619           |       1/21        1.0E-6     20          6           |    1000/21001    -4.7E-5     2           |       1/21        1.0E-6     1           
  12.125              |      97/8          0         7           4           |     982/81       -1.3E-4     2           |      97/8          0         1           
   5.5                |      11/2          0         1           1           |     995/181      -5.0E-4     2           |      11/2          0         1           
   0.1233333333333    |       9/73       -3.7E-4     16          8           |     971/7873     -3.4E-6     4           |       9/73       -3.7E-4     2           
   0.7454545454545    |      38/51       -4.8E-4     15          8           |     981/1316     -1.9E-5     6           |      38/51       -4.8E-4     4           
   0.01024801004      |       2/195       8.2E-4     98          9           |     488/47619     2.0E-8     13          |       2/195       8.2E-4     3           
   0.99011            |      91/92       -9.9E-4     91          8           |     801/809       1.3E-6     5           |     100/101      -1.1E-5     2           
   0.9901134545       |      91/92       -9.9E-4     91          8           |     601/607       1.9E-6     5           |     100/101      -1.5E-5     2           
   0.19999999         |       1/5         5.0E-8     4           3           |    1000/5001     -2.0E-4     2           |       1/5         5.0E-8     1           
   0.20000001         |       1/5        -5.0E-8     4           3           |    1000/4999      2.0E-4     3           |       1/5        -5.0E-8     2           
   5.0183168565E-05   |       1/19908     9.5E-4     19907       16          |    1000/19927001 -5.0E-8     2           |       1/19927     5.2E-12    1           
   3.909E-07          |       1/2555644   1.0E-3     2555643     23          |       1/1         2.6E6 (!)  1           |       1/2558199   1.1E-8     1           
88900003.001          |88900003/1        -1.1E-11    0           0           |88900004/1         1.1E-8     1           |88900003/1        -1.1E-11    0           
   0.26... (5/19)     |       5/19         0         7           6           |     996/3785     -5.3E-5     4           |       5/19         0         3           
   0.61... (37/61)    |      17/28        9.7E-4     8           7           |     982/1619     -1.7E-5     8           |      17/28        9.7E-4     5           
                      |                                                      |                                          | 
Accuracy: 1.0E-4      | Stern-Brocot                             OPTIMIZED   | Eppstein                                 | Richards                                 
Input                 | Result           Error       Iterations  Iterations  | Result           Error       Iterations  | Result           Error       Iterations  
======================| =====================================================| =========================================| =========================================
   0.62... (307/499)  |     227/369      -8.8E-5     33          11          |    9816/15955    -2.0E-7     8           |     299/486      -6.7E-6     6           
   0.05... (33/683)   |      23/476       6.4E-5     27          12          |    9989/206742    1.5E-7     7           |      23/476       6.4E-5     5           
   0.06... (33/541)   |      28/459       6.6E-5     24          12          |    9971/163464   -1.9E-7     6           |      33/541        0         5           
   1E-05              |       1/99991     9.0E-5     99990       18          |   10000/999999999 1.0E-9     3           |       1/99999     1.0E-5     1           
   0.333              |     303/910      -9.9E-5     305         12          |    9991/30003     1.0E-7     3           |     333/1000       0         2           
   0.7777             |     556/715      -1.0E-4     84          12          |    7777/10000      0         8           |    1109/1426     -1.8E-7     4           
   3.14... (pi)       |     289/92       -9.2E-5     19          8           |    9918/3157     -8.1E-7     4           |     333/106      -2.6E-5     2           
   2.72... (e)        |     193/71        1.0E-5     10          9           |    9620/3539      6.3E-8     11          |     193/71        1.0E-5     7           
   0.7454545454545    |      41/55        6.1E-14    16          8           |    9960/13361    -1.8E-6     6           |      41/55        6.1E-14    5           
   0.01024801004      |       7/683       8.7E-5     101         12          |    9253/902907   -1.3E-10    16          |       7/683       8.7E-5     5           
   0.99011            |     100/101      -1.1E-5     100         8           |     901/910      -1.1E-7     6           |     100/101      -1.1E-5     2           
   0.9901134545       |     100/101      -1.5E-5     100         8           |    8813/8901      1.6E-8     7           |     100/101      -1.5E-5     2           
   0.26... (5/19)     |       5/19         0         7           6           |    9996/37985    -5.3E-6     4           |       5/19         0         3           
   0.61... (37/61)    |      37/61         0         10          8           |    9973/16442    -1.6E-6     8           |      37/61         0         7           

性能比较

我进行了详细的速度测试并绘制了结果。不看质量,只看速度:

Stern-Brocot 优化 最多可将其减慢 2 倍,但原始 Stern-Brocot 在达到上述不幸值时可能会慢数百或数千倍。虽然每次调用仍然只有几微秒。 Richards 一直很快。 Eppstein 比其他人慢 3 倍左右。

Stern-Brocot 和 Richards 比较:

两者都返回不错的分数。 Richards 通常会导致较小的错误。它也快了一点。 Stern-Brocot 沿着 S-B 树走。它找到满足所需精度的最低分母的分数,然后停止。

如果您不需要最小分母分数,Richards 是一个不错的选择。

【讨论】:

迭代次数变大的原因是因为要达到 1/100,您正在尝试 1/2, 1/3, 1/4, ...而不是一旦开始下降树的一侧,您可以通过不更改那一侧来进行二进制搜索。这将为您提供 1/2、1/4、1/8、1/16、1/32、1/64、1/128、1/96、1/112、1/104、1/100。好多了。我没有在我的回答中实现这个技巧,因为我试图解释,而不是优化。 @btilly 我知道您很久以前就回答了这个问题,但我想知道您可以指出我在哪里可以找到有关此优化的信息。我不明白你的意思,我找不到信息。也许您可以使用链接或更详细的说明更新您的answer @PinkFloyd 对不起。直到现在我才看到你的评论。我的优化是基于我自己的想法,所以没有提供外部链接。然而,Kay Zed 显然理解并正确实施了它。希望这会有所帮助。【参考方案3】:

我知道你说你在网上搜索过,但如果你错过了下面的论文,它可能会有所帮助。它包含一个 Pascal 代码示例。

Algorithm To Convert A Decimal To A Fraction*

另外,作为标准库的一部分,Ruby 有处理有理数的代码。它可以从浮点数转换为有理数,反之亦然。我相信您也可以查看代码。该文档位于here。我知道您没有使用 Ruby,但查看算法可能会有所帮助。

此外,如果您使用在 .net 框架之上运行的 IronRuby,则可以从 C# 调用 Ruby 代码(甚至在 C# 代码文件中编写 Ruby 代码)。

*更新到新链接,因为原来的 URL 似乎已损坏 (http://homepage.smc.edu/kennedy_john/DEC2FRAC.pdf)

【讨论】:

这真的是一篇很棒的文章,我认为这是大多数人都在使用的,但是碰巧我下载他的代码(将代码翻译成c#)的人没有很好地理解它。我现在就测试一下,:) 链接已失效。 404,再次(在 https://sites.google.com/site/johnkennedyshome/home/downloadable-papers/dec2frac.pdf 找到另一个)【参考方案4】:

我找到了 Matt 引用的同一篇论文,我花了一秒钟时间用 Python 实现了它。也许在代码中看到相同的想法会更清楚。诚然,您要求用 C# 给出答案,而我正在用 Python 给您,但这是一个相当简单的程序,我相信它很容易翻译。参数是num(您要转换为有理数的十进制数)和epsilonnum 与计算出的有理数之间的最大允许差异)。一些快速测试运行发现,当epsilon 在 1e-4 左右时,通常只需要两到三次迭代即可收敛。

def dec2frac(num, epsilon, max_iter=20):
    d = [0, 1] + ([0] * max_iter)
    z = num
    n = 1
    t = 1

    while num and t < max_iter and abs(n/d[t] - num) > epsilon:
        t += 1
        z = 1/(z - int(z))
        d[t] = d[t-1] * int(z) + d[t-2]
        # int(x + 0.5) is equivalent to rounding x.
        n = int(num * d[t] + 0.5)

    return n, d[t]

编辑:我刚刚注意到您关于希望它们使用循环小数的说明。我不知道任何具有支持循环小数的语法的语言,所以我不确定如何处理它们,但是通过这种方法运行 0.6666666 和 0.166666 会返回正确的结果(2/3 和 1/6,分别)。

另一个编辑(我没想到这会这么有趣!):如果你想了解更多关于这个算法背后的理论,Wikipedia has an excellent page on the Euclidian algorithm

【讨论】:

你不需要数组,顺便说一句;一旦表达了与 Python 生成器相同的算法(这也避免了核心逻辑中对 epsilon 和 max_iter 的需要),我在 SO 的某个地方发布了一个答案。 啊,这里:***.com/questions/445113/… 是的,最初我只使用 d0 和 d1,但可读性较差,因此我使用了列表。此外,如果你把 max_iter 和 epsilon 取出来,它们只会被移到别处,我认为 API 用户在单个函数调用中完成整个事情会更方便,而不是要求调用者自己进行迭代。 【参考方案5】:

这个问题最流行的解决方案是 Richards’ algorithm 和 the Stern-Brocot algorithm,由 btilly 和 speed optimalization 由 btilly 和 Jay Zed 实现。 Richards 的算法是最快的,但不保证返回最好的分数。

我有一个解决这个问题的方法,它总是给出最好的分数,而且比上述所有算法都快。这是 C# 中的算法(下面是解释和速度测试)。

这是一个没有 cmets 的简短算法。文末源码中提供了完整版本。

public static Fraction DoubleToFractionSjaak(double value, double accuracy)

    int sign = value < 0 ? -1 : 1;
    value = value < 0 ? -value : value;
    int integerpart = (int)value;
    value -=  integerpart;
    double minimalvalue = value - accuracy;
    if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);
    double maximumvalue = value + accuracy;
    if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);
    int a = 0;
    int b = 1;
    int c = 1;
    int d = (int)(1 / maximumvalue);
    while (true)
    
        int n = (int)((b * minimalvalue - a) / (c - d * minimalvalue));
        if (n == 0) break;
        a += n * c;
        b += n * d;
        n = (int)((c - d * maximumvalue) / (b * maximumvalue - a));
        if (n == 0) break;
        c += n * a;
        d += n * b;
    
    int denominator = b + d;
    return new Fraction(sign * (integerpart * denominator + (a + c)), denominator);

其中 Fraction 是一个存储分数的简单类,如下所示:

public class Fraction

    public int Numerator  get; private set; 
    public int Denominator  get; private set; 

    public Fraction(int numerator, int denominator)
    
        Numerator = numerator;
        Denominator = denominator;
         

工作原理

与提到的其他解决方案一样,我的解决方案基于连分数。其他解决方案,例如来自 Eppstein 的解决方案或基于重复小数的解决方案,被证明速度较慢和/或产生次优结果。

连分数 基于连分数的解决方案大多基于两种算法,这两种算法都在 Ian Richards 于 1981 年发表的here 的一篇文章中进行了描述。他称它们为“慢连分数算法”和“快速连分数算法”。第一种称为 Stern-Brocot 算法,而后者称为 Richards 算法。

我的算法(简短说明) 要完全理解我的算法,您需要阅读 Ian Richards 的文章,或者至少了解什么是 Farey 对。此外,请阅读本文末尾的 cmets 算法。

该算法使用一个 Farey 对,包含一个左分数和一个右分数。通过反复取中位数,它正在接近目标值。这就像慢速算法,但有两个主要区别:

    只要中位数保持在目标值的一侧,就会一次执行多次迭代。 左右分数不能比给定精度更接近目标值。

交替检查目标值的右侧和左侧。如果算法不能产生更接近目标值的结果,则该过程结束。得到的中位数是最优解。

速度测试

我使用以下算法在笔记本电脑上进行了一些速度测试:

    Kay Zed and btilly 改进了慢速算法 John Kennedy 的 Fast 算法实现,由 Kay Zed 转换为 C# 我对 Fast 算法的实现(接近 Ian Richards 的原作) Jeremy Herrman’s Fast 算法的实现 我上面的算法

我省略了 btilly 原来的慢速算法,因为它在最坏情况下的性能很差。

测试集 我选择了一组目标值(非常随意)并以 5 种不同的精度计算了 100000 次分数。因为可能某些(未来的)算法无法处理假分数,所以只测试了从 0.0 到 1.0 的目标值。精度取自小数点后 2 到 6 位(0.005 到 0.0000005)。使用了以下集合:

0.999999, 0.000001, 0.25
0.33, 0.333, 0.3333, 0.33333, 0.333333, 0.333333333333, 
0.666666666666, 0.777777777777, 0.090909090909, 0.263157894737,
0.606557377049, 0.745454545454, 0.000050183168565,
pi - 3, e - 2.0, sqrt(2) - 1

结果

我进行了 13 次试运行。结果是整个数据集所需的毫秒数。

    Run 1   Run 2   Run 3   Run 4   Run 5   Run 6   Run 7   Run 8   Run 9   Run 10  Run 11  Run 12  Run 13
1.  9091    9222    9070    9111    9091    9108    9293    9118    9115    9113    9102    9143    9121
2.  7071    7125    7077    6987    7126    6985    7037    6964    7023    6980    7053    7050    6999
3.  6903    7059    7062    6891    6942    6880    6882    6918    6853    6918    6893    6993    6966
4.  7546    7554    7564    7504    7483    7529    7510    7512    7517    7719    7513    7520    7514
5.  6839    6951    6882    6836    6854    6880    6846    7017    6874    6867    6828    6848    6864

结论(跳过分析) 即使没有统计分析,也很容易看出我的算法比其他经过测试的算法更快。然而,与“快速算法”的最快变体的差异不到 1%。改进的慢速算法比最快算法慢 30%-35%”。

另一方面,即使是最慢的算法,平均也能在不到一微秒的时间内完成计算。因此,在正常情况下,速度并不是真正的问题。在我看来,最好的算法主要是个人喜好问题,因此请根据其他标准选择任何经过测试的算法。

算法是否给出了最好的结果? 算法是否支持我最喜欢的语言? 算法的代码大小是多少? 算法是否可读、易理解?

源代码

下面的源代码包含所有使用的算法。它包括:

我的原始算法(使用 cmets) 我的算法的更快版本(但可读性较差) 原来的慢速算法 所有经过测试的算法
public class DoubleToFraction

    // ===================================================
    // Sjaak algorithm - original version
    //

    public static Fraction SjaakOriginal(double value, double accuracy)
    
        // Split value in a sign, an integer part, a fractional part
        int sign = value < 0 ? -1 : 1;
        value = value < 0 ? -value : value;
        int integerpart = (int)value;
        value -= integerpart;

        // check if the fractional part is near 0
        double minimalvalue = value - accuracy;
        if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);

        // check if the fractional part is near 1
        double maximumvalue = value + accuracy;
        if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);

        // The left fraction (a/b) is initially (0/1), the right fraction (c/d) is initially (1/1)
        // Together they form a Farey pair.
        // We will keep the left fraction below the minimumvalue and the right fraction above the maximumvalue
        int a = 0;
        int b = 1;
        int c = 1;
        int d = (int)(1 / maximumvalue);

        // The first interation is performed above. Calculate maximum n where (n*a+c)/(n*b+d) >= maximumvalue 
        // This is the same as n <= 1/maximumvalue - 1, d will become n+1 = floor(1/maximumvalue)

        // repeat forever (at least until we cannot close in anymore)
        while (true)
        
            // Close in from the left n times. 
            // Calculate maximum n where (a+n*c)/(b+n*d) <= minimalvalue
            // This is the same as n <= (b * minimalvalue - a) / (c-d*minimalvalue)
            int n = (int)((b * minimalvalue - a) / (c - d * minimalvalue));

            // If we cannot close in from the left (and also not from the right anymore) the loop ends
            if (n == 0) break;

            // Update left fraction
            a += n * c;
            b += n * d;

            // Close in from the right n times.
            // Calculate maximum n where (n*a+c)/(n*b+d) >= maximumvalue
            // This is the same as n <= (c - d * maximumvalue) / (b * maximumvalue - a)
            n = (int)((c - d * maximumvalue) / (b * maximumvalue - a));

            // If we cannot close in from the right (and also not from the left anymore) the loop ends
            if (n == 0) break;

            // Update right fraction
            c += n * a;
            d += n * b;
        

        // We cannot close in anymore
        // The best fraction will be the mediant of the left and right fraction = (a+c)/(b+d)
        int denominator = b + d;
        return new Fraction(sign * (integerpart * denominator + (a + c)), denominator);

    

    // ===================================================
    // Sjaak algorithm - faster version
    //

    public static Fraction SjaakFaster(double value, double accuracy)
    
        int sign = value < 0 ? -1 : 1;
        value = value < 0 ? -value : value;
        int integerpart = (int)value;
        value -= integerpart;
        double minimalvalue = value - accuracy;
        if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);
        double maximumvalue = value + accuracy;
        if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);
        //int a = 0;
        int b = 1;
        //int c = 1;
        int d = (int)(1 / maximumvalue);
        double left_n = minimalvalue; // b * minimalvalue - a
        double left_d = 1.0 - d * minimalvalue; // c - d * minimalvalue
        double right_n = 1.0 - d * maximumvalue; // c - d * maximumvalue
        double right_d = maximumvalue; // b * maximumvalue - a            
        while (true)
        
            if (left_n < left_d) break;
            int n = (int)(left_n / left_d);
            //a += n * c;
            b += n * d;
            left_n -= n * left_d;
            right_d -= n * right_n;
            if (right_n < right_d) break;
            n = (int)(right_n / right_d);
            //c += n * a;
            d += n * b;
            left_d -= n * left_n;
            right_n -= n * right_d;
        


        int denominator = b + d;
        int numerator = (int)(value * denominator + 0.5);
        return new Fraction(sign * (integerpart * denominator + numerator), denominator);
    

    // ===================================================
    // Original Farley - Implemented by btilly
    //

    public static Fraction OriginalFarley(double value, double accuracy)
    
        // Split value in a sign, an integer part, a fractional part
        int sign = value < 0 ? -1 : 1;
        value = value < 0 ? -value : value;
        int integerpart = (int)value;
        value -= integerpart;

        // check if the fractional part is near 0
        double minimalvalue = value - accuracy;
        if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);

        // check if the fractional part is near 1
        double maximumvalue = value + accuracy;
        if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);

        // The lower fraction is 0/1
        int lower_numerator = 0;
        int lower_denominator = 1;

        // The upper fraction is 1/1
        int upper_numerator = 1;
        int upper_denominator = 1;

        while (true)
        
            // The middle fraction is (lower_numerator + upper_numerator) / (lower_denominator + upper_denominator)
            int middle_numerator = lower_numerator + upper_numerator;
            int middle_denominator = lower_denominator + upper_denominator;

            if (middle_denominator * maximumvalue < middle_numerator)
            
                // real + error < middle : middle is our new upper
                upper_numerator = middle_numerator;
                upper_denominator = middle_denominator;
            
            else if (middle_numerator < minimalvalue * middle_denominator)
            
                // middle < real - error : middle is our new lower
                lower_numerator = middle_numerator;
                lower_denominator = middle_denominator;
            
            else
            
                return new Fraction(sign * (integerpart * middle_denominator + middle_numerator), middle_denominator);
            
        
    

    // ===================================================
    // Modified Farley - Implemented by btilly, Kay Zed
    //

    public static Fraction ModifiedFarley(double value, double accuracy)
    
        // Split value in a sign, an integer part, a fractional part
        int sign = value < 0 ? -1 : 1;
        value = value < 0 ? -value : value;
        int integerpart = (int)value;
        value -= integerpart;

        // check if the fractional part is near 0
        double minimalvalue = value - accuracy;
        if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);

        // check if the fractional part is near 1
        double maximumvalue = value + accuracy;
        if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);

        // The lower fraction is 0/1
        int lower_numerator = 0;
        int lower_denominator = 1;

        // The upper fraction is 1/1
        int upper_numerator = 1;
        int upper_denominator = 1;

        while (true)
        
            // The middle fraction is (lower_numerator + upper_numerator) / (lower_denominator + upper_denominator)
            int middle_numerator = lower_numerator + upper_numerator;
            int middle_denominator = lower_denominator + upper_denominator;

            if (middle_denominator * maximumvalue < middle_numerator)
            
                // real + error < middle : middle is our new upper
                ModifiedFarleySeek(ref upper_numerator, ref upper_denominator, lower_numerator, lower_denominator, (un, ud) => (lower_denominator + ud) * maximumvalue < (lower_numerator + un));
            
            else if (middle_numerator < minimalvalue * middle_denominator)
            
                // middle < real - error : middle is our new lower
                ModifiedFarleySeek(ref lower_numerator, ref lower_denominator, upper_numerator, upper_denominator, (ln, ld) => (ln + upper_numerator) < minimalvalue * (ld + upper_denominator));
            
            else
            
                return new Fraction(sign * (integerpart * middle_denominator + middle_numerator), middle_denominator);
            
        
    

    private static void ModifiedFarleySeek(ref int a, ref int b, int ainc, int binc, Func<int, int, bool> f)
    
        // Binary seek for the value where f() becomes false
        a += ainc;
        b += binc;

        if (f(a, b))
        
            int weight = 1;

            do
            
                weight *= 2;
                a += ainc * weight;
                b += binc * weight;
            
            while (f(a, b));

            do
            
                weight /= 2;

                int adec = ainc * weight;
                int bdec = binc * weight;

                if (!f(a - adec, b - bdec))
                
                    a -= adec;
                    b -= bdec;
                
            
            while (weight > 1);
        
    

    // ===================================================
    // Richards implementation by Jemery Hermann
    //

    public static Fraction RichardsJemeryHermann(double value, double accuracy, int maxIterations = 20)
    

        // Split value in a sign, an integer part, a fractional part
        int sign = value < 0 ? -1 : 1;
        value = value < 0 ? -value : value;
        int integerpart = (int)value;
        value -= integerpart;

        // check if the fractional part is near 0
        double minimalvalue = value - accuracy;
        if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);

        // check if the fractional part is near 1
        double maximumvalue = value + accuracy;
        if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);

        // Richards - Implemented by Jemery Hermann
        double[] d = new double[maxIterations + 2];
        d[1] = 1;
        double z = value;
        double n = 1;
        int t = 1;

        while (t < maxIterations && Math.Abs(n / d[t] - value) > accuracy)
        
            t++;
            z = 1 / (z - (int)z);
            d[t] = d[t - 1] * (int)z + d[t - 2];
            n = (int)(value * d[t] + 0.5);
        

        return new Fraction(sign * (integerpart * (int)d[t] + (int)n), (int)d[t]);
    

    // ===================================================
    // Richards implementation by Kennedy
    //

    public static Fraction RichardsKennedy(double value, double accuracy)
    
        // Split value in a sign, an integer part, a fractional part
        int sign = value < 0 ? -1 : 1;
        value = value < 0 ? -value : value;
        int integerpart = (int)value;
        value -= integerpart;

        // check if the fractional part is near 0
        double minimalvalue = value - accuracy;
        if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);

        // check if the fractional part is near 1
        double maximumvalue = value + accuracy;
        if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);

        // Richards
        double z = value;
        int previousDenominator = 0;
        int denominator = 1;
        int numerator;
        do
        
            z = 1.0 / (z - (int)z);
            int temp = denominator;
            denominator = denominator * (int)z + previousDenominator;
            previousDenominator = temp;
            numerator = (int)(value * denominator + 0.5);
        
        while (Math.Abs(value - (double)numerator / denominator) > accuracy && z != (int)z);

        return new Fraction(sign * (integerpart * denominator + numerator), denominator);
    

    // ===================================================
    // Richards implementation by Sjaak
    //

    public static Fraction RichardsOriginal(double value, double accuracy)
    
        // Split value in a sign, an integer part, a fractional part
        int sign = value < 0 ? -1 : 1;
        value = value < 0 ? -value : value;
        int integerpart = (int)value;
        value -= integerpart;

        // check if the fractional part is near 0
        double minimalvalue = value - accuracy;
        if (minimalvalue < 0.0) return new Fraction(sign * integerpart, 1);

        // check if the fractional part is near 1
        double maximumvalue = value + accuracy;
        if (maximumvalue > 1.0) return new Fraction(sign * (integerpart + 1), 1);

        // Richards
        double z = value;
        int denominator0 = 0;
        int denominator1 = 1;
        int numerator0 = 1;
        int numerator1 = 0;
        int n = (int)z;
        while (true)
        
            z = 1.0 / (z - n);
            n = (int)z;

            int temp = denominator1;
            denominator1 = denominator1 * n + denominator0;
            denominator0 = temp;

            temp = numerator1;
            numerator1 = numerator1 * n + numerator0;
            numerator0 = temp;

            double d = (double)numerator1 / denominator1;
            if (d > minimalvalue && d < maximumvalue) break;
        
        return new Fraction(sign * (integerpart * denominator1 + numerator1), denominator1);
    


【讨论】:

【参考方案6】:

您无法在 .net 中表示循环小数,因此我将忽略您问题的这一部分。

您只能表示有限且相对较少的数字。

有一个非常简单的算法:

取小数x 统计小数点后的位数;打电话给n 创建一个分数(10^n * x) / 10^n 从分子和分母中去除公因数。

所以如果你有 0.44,你会数 2 位是小数点 - n = 2,然后写

(0.44 * 10^2) / 10^2 = 44 / 100 因式分解(去除 4 的公因数)给出11 / 25

【讨论】:

很好,但是您可以检测到小数是否在 .net 中重复出现,对吗?我已经尝试过这样的事情,这不是我想要的。另外,您知道找出和去除公因数的最佳方法吗? 是否可以检测小数是否重复出现是无关紧要的,因为你不能有重复出现的小数。 decimal 类型根本不可能重复出现。 嗯。看来我将需要更多的大众 tuts:o 你到底想告诉我什么?? 您使用的是 .net,其中十进制类型可以少于 30 位。它不能有无限位数。它无法表示“重复”模式。你可以有 0.333333333333333333 但你不能有 0.3*(重复)——它们不是一回事。 0.3* 是 1/3,但前者是 33333333(etc)/1000000 - 略小于 1/3。 机器只能知道你告诉它什么——所以如果你想定义一些规则来将笨拙的 20 位小数“四舍五入”为一个不错的小数,你可以:如果有超过 10 个数字,并且有一个 1 或 2 位数的分数在 0.1% 或其他一些边距内,然后将其四舍五入。但这些规则由您决定。事实仍然是 0.33333333333333333333 与 1/3 不同。【参考方案7】:

这是 Will Brown 的 Python 示例的 C# 版本。我还对其进行了更改以处理单独的整数(例如“2 1/8”而不是“17/8”)。

    public static string DoubleToFraction(double num, double epsilon = 0.0001, int maxIterations = 20)
    
        double[] d = new double[maxIterations + 2];
        d[1] = 1;
        double z = num;
        double n = 1;
        int t = 1;

        int wholeNumberPart = (int)num;
        double decimalNumberPart = num - Convert.ToDouble(wholeNumberPart);

        while (t < maxIterations && Math.Abs(n / d[t] - num) > epsilon)
        
            t++;
            z = 1 / (z - (int)z);
            d[t] = d[t - 1] * (int)z + d[t - 2];
            n = (int)(decimalNumberPart * d[t] + 0.5);
        

        return string.Format((wholeNumberPart > 0 ? wholeNumberPart.ToString() + " " : "") + "0/1",
                             n.ToString(),
                             d[t].ToString()
                            );
    

【讨论】:

【参考方案8】:

我编写了一个运行相当快的快速类,并给出了我期望的结果。您也可以选择您的精度。它比我看到的任何代码都简单得多,而且运行速度也很快。

//Written By Brian Dobony
public static class Fraction

    public static string ConvertDecimal(Double NumberToConvert, int DenominatorPercision = 32)
    
        int WholeNumber = (int)NumberToConvert;
        double DecimalValue = NumberToConvert - WholeNumber;

        double difference = 1;
        int numerator = 1;
        int denominator = 1;

        // find closest value that matches percision
        // Automatically finds Fraction in simplified form
        for (int y = 2; y < DenominatorPercision + 1; y++)
        
                for (int x = 1; x < y; x++)
                
                    double tempdif = Math.Abs(DecimalValue - (double)x / (double)y);
                    if (tempdif < difference)
                    
                        numerator = x;
                        denominator = y;
                        difference = tempdif;
                        // if exact match is found return it
                        if (difference == 0)
                        
                            return FractionBuilder(WholeNumber, numerator, denominator);
                        
                    
                
        
        return FractionBuilder(WholeNumber, numerator, denominator);
    

    private static string FractionBuilder(int WholeNumber, int Numerator, int Denominator)
    
        if (WholeNumber == 0)
        
            return Numerator + @"/" + Denominator;
        
        else
        
            return WholeNumber + " " + Numerator + @"/" + Denominator;
        
    

【讨论】:

我试过这个程序,它对'seamingly'重复小数很有用,但它对某些分数没有像我预期的那样工作,例如:当我使用值时:0.068376968,精度为32,结果是 2/29 =.068965517,这仅适用于小数点后 4 位。不过,对我来说没问题。【参考方案9】:

这是Ian Richards / John Kennedy 的算法的 C# 版本。此处使用相同算法的其他答案:

Matt(仅限肯尼迪论文的链接) Haldean Brown (Python) Jeremy Herrman (C#) PinkFloyd(C)

它不处理无穷大和 NaN。

这个算法

示例值和与其他算法的比较,请参阅my other answer

public Fraction RealToFraction(double value, double accuracy)

    if (accuracy <= 0.0 || accuracy >= 1.0)
    
        throw new ArgumentOutOfRangeException("accuracy", "Must be > 0 and < 1.");
    

    int sign = Math.Sign(value);

    if (sign == -1)
    
        value = Math.Abs(value);
    

    // Accuracy is the maximum relative error; convert to absolute maxError
    double maxError = sign == 0 ? accuracy : value * accuracy;

    int n = (int) Math.Floor(value);
    value -= n;

    if (value < maxError)
    
        return new Fraction(sign * n, 1);
    

    if (1 - maxError < value)
    
        return new Fraction(sign * (n + 1), 1);
    

    double z = value;
    int previousDenominator = 0;
    int denominator = 1;
    int numerator;

    do
    
        z = 1.0 / (z - (int) z);
        int temp = denominator;
        denominator = denominator * (int) z + previousDenominator;
        previousDenominator = temp;
        numerator = Convert.ToInt32(value * denominator);
    
    while (Math.Abs(value - (double) numerator / denominator) > maxError && z != (int) z);

    return new Fraction((n * denominator + numerator) * sign, denominator);

【讨论】:

【参考方案10】:

我想出了一个很晚的答案。代码取自an article from Richards published in 1981,写在c

inline unsigned int richards_solution(double const& x0, unsigned long long& num, unsigned long long& den, double& sign, double const& err = 1e-10)
    sign = my::sign(x0);
    double g(std::abs(x0));
    unsigned long long a(0);
    unsigned long long b(1);
    unsigned long long c(1);
    unsigned long long d(0);
    unsigned long long s;
    unsigned int iter(0);
    do 
        s = std::floor(g);
        num = a + s*c;
        den = b + s*d;
        a = c;
        b = d;
        c = num;
        d = den;
        g = 1.0/(g-s);
        if(err>std::abs(sign*num/den-x0)) return iter; 
     while(iter++<1e6);
    std::cerr<<__PRETTY_FUNCTION__<<" : failed to find a fraction for "<<x0<<std::endl;
    return 0;

我在这里重写了我对btilly_solution 的实现:

inline unsigned int btilly_solution(double x, unsigned long long& num, unsigned long long& den, double& sign, double const& err = 1e-10)
    sign = my::sign(x);
    num  = std::floor(std::abs(x));
    x = std::abs(x)-num;
    unsigned long long lower_n(0);
    unsigned long long lower_d(1);
    unsigned long long upper_n(1);
    unsigned long long upper_d(1);
    unsigned long long middle_n;
    unsigned long long middle_d;
    unsigned int iter(0);
    do 
        middle_n = lower_n + upper_n;
        middle_d = lower_d + upper_d;
        if(middle_d*(x+err)<middle_n)
            upper_n = middle_n;
            upper_d = middle_d;
         else if(middle_d*(x-err)>middle_n) 
            lower_n = middle_n;
            lower_d = middle_d;
         else 
            num = num*middle_d+middle_n;
            den = middle_d;
            return iter;
        
     while(iter++<1e6);
    den = 1;
    std::cerr<<__PRETTY_FUNCTION__<<" : failed to find a fraction for "<<x+num<<std::endl;
    return 0;

在这里我提出了一些错误为1e-10 的测试:

------------------------------------------------------ |
btilly  0.166667 0.166667=1/6 in 5 iterations          | 1/6
richard 0.166667 0.166667=1/6 in 1 iterations          |
------------------------------------------------------ |
btilly  0.333333 0.333333=1/3 in 2 iterations          | 1/3
richard 0.333333 0.333333=1/3 in 1 iterations          |
------------------------------------------------------ |
btilly  0.142857 0.142857=1/7 in 6 iterations          | 1/7
richard 0.142857 0.142857=1/7 in 1 iterations          |
------------------------------------------------------ |
btilly  0.714286 0.714286=5/7 in 4 iterations          | 5/7
richard 0.714286 0.714286=5/7 in 4 iterations          |
------------------------------------------------------ |
btilly  1e-07 1.001e-07=1/9990010 in 9990009 iteration | 0.0000001
richard 1e-07 1e-07=1/10000000 in 1 iterations         |
------------------------------------------------------ |
btilly  3.66667 3.66667=11/3 in 2 iterations           | 11/3
richard 3.66667 3.66667=11/3 in 3 iterations           |
------------------------------------------------------ |
btilly  1.41421 1.41421=114243/80782 in 25 iterations  | sqrt(2)
richard 1.41421 1.41421=114243/80782 in 13 iterations  |
------------------------------------------------------ |
btilly  3.14159 3.14159=312689/99532 in 317 iterations | pi
richard 3.14159 3.14159=312689/99532 in 7 iterations   |
------------------------------------------------------ |
btilly  2.71828 2.71828=419314/154257 in 36 iterations | e
richard 2.71828 2.71828=517656/190435 in 14 iterations |
------------------------------------------------------ |
btilly  0.390885 0.390885=38236/97819 in 60 iterations | random
richard 0.390885 0.390885=38236/97819 in 13 iterations |

如您所见,这两种方法的结果大致相同,但 richards 的方法更高效且更易于实施。

编辑

要编译我的代码,您需要对 my::sign 进行定义,这只是一个 返回变量符号的函数。这是我的实现

 namespace my
    template<typename Type> inline constexpr
        int sign_unsigned(Type x) return Type(0)<x; 

    template<typename Type> inline constexpr
        int sign_signed(Type x) return (Type(0)<x)-(x<Type(0)); 

    template<typename Type> inline constexpr
        int sign(Type x)  return std::is_signed<Type>()?sign_signed(x):sign_unsigned(x); 
 

对不起

我猜this answer 指的是相同的算法。我以前没见过……

【讨论】:

【参考方案11】:

这个算法由加州大学欧文分校的 David Eppstein 提出,基于连分数理论,最初是用 C 语言编写的,后来被我翻译成 C#。它生成的分数满足误差范围,但大多数看起来不如我其他答案中的解决方案。例如。 0.5 变为 999/19991/2 在显示给用户时将是首选(如果需要,请参阅我的other answers)。

将误差幅度指定为双精度值(相对于值,而不是绝对误差)存在重载。对于Fraction 类型,请参阅我的其他答案。

顺便说一句,如果您的分数可能变大,请将相关的ints 更改为long。与其他算法相比,这种算法容易溢出。

示例值和与其他算法的比较,请参阅my other answer

public Fraction RealToFraction(double value, int maxDenominator)

    // http://www.ics.uci.edu/~eppstein/numth/frap.c
    // Find rational approximation to given real number
    // David Eppstein / UC Irvine / 8 Aug 1993
    // With corrections from Arno Formella, May 2008

    if (value == 0.0)
    
        return new Fraction(0, 1);
    

    int sign = Math.Sign(value);

    if (sign == -1)
    
        value = Math.Abs(value);
    

    int[,] m =   1, 0 ,  0, 1  ;
    int ai = (int) value;

    // Find terms until denominator gets too big
    while (m[1, 0] * ai + m[1, 1] <= maxDenominator)
    
        int t = m[0, 0] * ai + m[0, 1];
        m[0, 1] = m[0, 0];
        m[0, 0] = t;
        t = m[1, 0] * ai + m[1, 1];
        m[1, 1] = m[1, 0];
        m[1, 0] = t;

        value = 1.0 / (value - ai);

        // 0x7FFFFFFF = Assumes 32 bit floating point just like in the C implementation.
        // This check includes Double.IsInfinity(). Even though C# double is 64 bits,
        // the algorithm sometimes fails when trying to increase this value too much. So
        // I kept it. Anyway, it works.
        if (value > 0x7FFFFFFF)
                            
            break;
        

        ai = (int) value;
    

    // Two approximations are calculated: one on each side of the input
    // The result of the first one is the current value. Below the other one
    // is calculated and it is returned.

    ai = (maxDenominator - m[1, 1]) / m[1, 0];
    m[0, 0] = m[0, 0] * ai + m[0, 1];
    m[1, 0] = m[1, 0] * ai + m[1, 1];

    return new Fraction(sign * m[0, 0], m[1, 0]);


public Fraction RealToFraction(double value, double accuracy)

    if (accuracy <= 0.0 || accuracy >= 1.0)
    
        throw new ArgumentOutOfRangeException("accuracy", "Must be > 0 and < 1.");
    

    int maxDenominator = (int) Math.Ceiling(Math.Abs(1.0 / (value * accuracy)));

    if (maxDenominator < 1)
    
        maxDenominator = 1;
    

    return RealToFraction(value, maxDenominator);

【讨论】:

【参考方案12】:

循环小数可以用两个有限小数表示:重复之前的左边部分和重复部分。例如。 1.6181818... = 1.6 + 0.1*(0.18...)。将其视为a + b * sum(c * 10**-(d*k) for k in range(1, infinity))(此处使用 Python 表示法)。在我的示例中,a=1.6b=0.1c=18d=2c 中的位数)。无限和可以简化(sum(r**k for r in range(1, infinity)) == r / (1 - r),如果我没记错的话),产生a + b * (c * 10**-d) / (1 - c * 10**-d)),一个有限的比率。也就是说,以有理数abcd 开头,最后得到另一个。

(这详细说明了 Kirk Broadhurst 的答案,就其而言,这是正确的,但不包括重复小数。我不保证我在上面没有犯任何错误,尽管我相信一般方法有效。)

【讨论】:

【参考方案13】:

我最近不得不执行这项任务,即使用存储在我们的 SQL Server 数据库中的十进制数据类型。在表示层,该值被编辑为文本框中的小数值。这里的复杂性在于使用十进制数据类型,与 int 或 long 相比,它拥有一些相当大的值。因此,为了减少数据溢出的机会,我在整个转换过程中坚持使用小数数据类型。

在开始之前,我想评论一下柯克之前的回答。只要没有任何假设,他就是绝对正确的。但是,如果开发人员只在十进制数据类型的范围内寻找重复模式,.3333333... 可以表示为 1/3。该算法的一个示例可以在basic-mathematics.com 找到。同样,这意味着您必须根据可用信息做出假设,并且使用此方法只能捕获非常小的重复小数子集。但是对于小数字应该没问题。

继续前进,让我给您简要介绍一下我的解决方案。如果您想阅读带有附加代码的完整示例,我创建了一个blog post,其中包含更多详细信息。

将十进制数据类型转换为字符串分数

public static void DecimalToFraction(decimal value, ref decimal sign, ref decimal numerator, ref decimal denominator)

    const decimal maxValue = decimal.MaxValue / 10.0M;

    // e.g. .25/1 = (.25 * 100)/(1 * 100) = 25/100 = 1/4
    var tmpSign = value < decimal.Zero ? -1 : 1;
    var tmpNumerator = Math.Abs(value);
    var tmpDenominator = decimal.One;

    // While numerator has a decimal value
    while ((tmpNumerator - Math.Truncate(tmpNumerator)) > 0 && 
        tmpNumerator < maxValue && tmpDenominator < maxValue)
    
        tmpNumerator = tmpNumerator * 10;
        tmpDenominator = tmpDenominator * 10;
    

    tmpNumerator = Math.Truncate(tmpNumerator); // Just in case maxValue boundary was reached.
    ReduceFraction(ref tmpNumerator, ref tmpDenominator);
    sign = tmpSign;
    numerator = tmpNumerator;
    denominator = tmpDenominator;


public static string DecimalToFraction(decimal value)

    var sign = decimal.One;
    var numerator = decimal.One;
    var denominator = decimal.One;
    DecimalToFraction(value, ref sign, ref numerator, ref denominator);
    return string.Format("0/1", (sign * numerator).ToString().TruncateDecimal(), 
        denominator.ToString().TruncateDecimal());

这非常简单,其中 DecimalToFraction(decimal value) 只不过是第一种方法的简化入口点,它提供对组成分数的所有组件的访问。如果您的小数点为 0.325,则将其除以 10 的小数位数。最后减少分数。而且,在本例中,0.325 = 325/10^3 = 325/1000 = 13/40。

接下来,换个方向。

将字符串分数转换为十进制数据类型

static readonly Regex FractionalExpression = new Regex(@"^(?<sign>[-])?(?<numerator>\d+)(/(?<denominator>\d+))?$");
public static decimal? FractionToDecimal(string fraction)

    var match = FractionalExpression.Match(fraction);
    if (match.Success)
    
        // var sign = Int32.Parse(match.Groups["sign"].Value + "1");
        var numerator = Int32.Parse(match.Groups["sign"].Value + match.Groups["numerator"].Value);
        int denominator;
        if (Int32.TryParse(match.Groups["denominator"].Value, out denominator))
            return denominator == 0 ? (decimal?)null : (decimal)numerator / denominator;
        if (numerator == 0 || numerator == 1)
            return numerator;
    
    return null;

转换回小数也很简单。在这里,我们解析出小数部分,将它们存储在我们可以使用的东西中(这里是十进制值)并执行我们的除法。

【讨论】:

【参考方案14】:

我的 2 美分。这是btilly优秀算法的VB.NET版本:

   Public Shared Sub float_to_fraction(x As Decimal, ByRef Numerator As Long, ByRef Denom As Long, Optional ErrMargin As Decimal = 0.001)
    Dim n As Long = Int(Math.Floor(x))
    x -= n

    If x < ErrMargin Then
        Numerator = n
        Denom = 1
        Return
    ElseIf x >= 1 - ErrMargin Then
        Numerator = n + 1
        Denom = 1
        Return
    End If

    ' The lower fraction is 0/1
    Dim lower_n As Integer = 0
    Dim lower_d As Integer = 1
    ' The upper fraction is 1/1
    Dim upper_n As Integer = 1
    Dim upper_d As Integer = 1

    Dim middle_n, middle_d As Decimal
    While True
        ' The middle fraction is (lower_n + upper_n) / (lower_d + upper_d)
        middle_n = lower_n + upper_n
        middle_d = lower_d + upper_d
        ' If x + error < middle
        If middle_d * (x + ErrMargin) < middle_n Then
            ' middle is our new upper
            upper_n = middle_n
            upper_d = middle_d
            ' Else If middle < x - error
        ElseIf middle_n < (x - ErrMargin) * middle_d Then
            ' middle is our new lower
            lower_n = middle_n
            lower_d = middle_d
            ' Else middle is our best fraction
        Else
            Numerator = n * middle_d + middle_n
            Denom = middle_d
            Return
        End If
    End While
End Sub

【讨论】:

【参考方案15】:

好吧,看来我终于不得不自己动手了。我只需要创建一个程序来模拟我自己解决它的自然方式。我刚刚将代码提交给 codeproject,因为在这里写出整个代码不合适。您可以从这里Fraction_Conversion 下载项目,或查看the codeproject page here。

它是这样工作的:

    找出给定的小数是否为负数 将十进制转换为绝对值 获取给定小数的整数部分 获取小数部分 检查小数是否重复出现。如果小数重复出现,则我们返回精确的重复小数 如果小数不重复出现,则通过将分子更改为 10^no 来开始减少。十进制,否则我们从分子中减去 1 然后减少分数

代码预览:

    private static string dec2frac(double dbl)
    
        char neg = ' ';
        double dblDecimal = dbl;
        if (dblDecimal == (int) dblDecimal) return dblDecimal.ToString(); //return no if it's not a decimal
        if (dblDecimal < 0)
        
            dblDecimal = Math.Abs(dblDecimal);
            neg = '-';
        
        var whole = (int) Math.Truncate(dblDecimal);
        string decpart = dblDecimal.ToString().Replace(Math.Truncate(dblDecimal) + ".", "");
        double rN = Convert.ToDouble(decpart);
        double rD = Math.Pow(10, decpart.Length);

        string rd = recur(decpart);
        int rel = Convert.ToInt32(rd);
        if (rel != 0)
        
            rN = rel;
            rD = (int) Math.Pow(10, rd.Length) - 1;
        
        //just a few prime factors for testing purposes
        var primes = new[] 41, 43, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2;
        foreach (int i in primes) reduceNo(i, ref rD, ref rN);

        rN = rN + (whole*rD);
        return string.Format("01/2", neg, rN, rD);
    

感谢 @Darius 让我知道如何解决循环小数 :)

【讨论】:

您将如何处理具有循环小数且在适合浮点的周期内不重复出现的分数?即使是相当小的分数也会发生这种情况。 @btilly:这是很久以前的事了,只是解决问题的一种相当简单的方法,也是当时可接受的最佳解决方案。更好的解决方案是使用 BigInteger 类。它适用于我测试过的所有分数,也许您可​​以按照您的建议使用这些分数自己尝试一下。 我不同意“最佳可接受的解决方案”,因为我的解决方案在您之前发布,更短,被更多人赞成,处理您没有的分数,并且可证明在所有情况下都提出了最佳分数而你的没有。我不确定您使用的“最佳”定义是什么。 我确实很欣赏您的解决方案,但它不是在 C# 中,也不是在任何其他中。如果Jeremy's solution 可用,我会接受它。【参考方案16】:

这是一个用 VB 实现的算法,它可以转换我多年前写的 Floating Point Decimal to Integer Fraction。

基本上,您从分子 = 0 和分母 = 1 开始,然后如果商小于输入的小数,则将分子加 1,如果商大于输入的小数,则将分母加 1。重复直到达到您想要的精度。

【讨论】:

【参考方案17】:

如果我是你,我会处理“.NET 中没有重复小数”的问题,方法是让它转换带有某种重复标记的字符串。

例如1/3 可以表示为“0.R3” 1/60 可以表示为“0.01R6”

我需要从 double 或 decimal 进行显式转换,因为这样的值只能转换为接近的分数。从 int 隐式转换是可以的。

您可以使用结构并将分数 (f) 存储在两个长整数 p 和 q 中,使得 f=p/q、q!=0 和 gcd(p, q) == 1。

【讨论】:

【参考方案18】:

这里可以有小数转小数的方法:

/// <summary>
    /// Converts Decimals into Fractions.
    /// </summary>
    /// <param name="value">Decimal value</param>
    /// <returns>Fraction in string type</returns>
    public string DecimalToFraction(double value)
    
        string result;
        double numerator, realValue = value;
        int num, den, decimals, length;
        num = (int)value;
        value = value - num;
        value = Math.Round(value, 5);
        length = value.ToString().Length;
        decimals = length - 2;
        numerator = value;
        for (int i = 0; i < decimals; i++)
        
            if (realValue < 1)
            
                numerator = numerator * 10;
            
            else
            
                realValue = realValue * 10;
                numerator = realValue;
            
        
        den = length - 2;
        string ten = "1";
        for (int i = 0; i < den; i++)
        
            ten = ten + "0";
        
        den = int.Parse(ten);
        num = (int)numerator;
        result = SimplifiedFractions(num, den);
        return result;
    

    /// <summary>
    /// Converts Fractions into Simplest form.
    /// </summary>
    /// <param name="num">Numerator</param>
    /// <param name="den">Denominator</param>
    /// <returns>Simplest Fractions in string type</returns>
    string SimplifiedFractions(int num, int den)
    
        int remNum, remDen, counter;
        if (num > den)
        
            counter = den;
        
        else
        
            counter = num;
        
        for (int i = 2; i <= counter; i++)
        
            remNum = num % i;
            if (remNum == 0)
            
                remDen = den % i;
                if (remDen == 0)
                
                    num = num / i;
                    den = den / i;
                    i--;
                
            
        
        return num.ToString() + "/" + den.ToString();
    

【讨论】:

【参考方案19】:

这是我不久前为一个项目编写的算法。它采用了一种不同的方法,它更类似于你用手做的事情。我不能保证它的效率,但它可以完成工作。

    public static string toFraction(string exp) 
        double x = Convert.ToDouble(exp);
        int sign = (Math.Abs(x) == x) ? 1 : -1;
        x = Math.Abs(x);
        int n = (int)x; // integer part
        x -= n; // fractional part
        int mult, nm, dm;
        int decCount = 0;

        Match m = Regex.Match(Convert.ToString(x), @"([0-9]+?)\1+.?$");
        // repeating fraction
        if (m.Success) 
            m = Regex.Match(m.Value, @"([0-9]+?)(?=\1)");
            mult = (int)Math.Pow(10, m.Length);

            // We have our basic fraction
            nm = (int)Math.Round(((x * mult) - x));
            dm = mult - 1;
        
        // get the number of decimal places
        else 
            double t = x;
            while (t != 0) 
                decCount++;
                t *= 10;
                t -= (int)t;
            
            mult = (int)Math.Pow(10, decCount);

            // We have our basic fraction
            nm = (int)((x * mult));
            dm = mult;
        
        // can't be simplified
        if (nm < 0 || dm < 0) return exp;

        //Simplify
        Stack factors = new Stack();
        for (int i = 2; i < nm + 1; i++) 
            if (nm % i == 0) factors.Push(i);  // i is a factor of the numerator
        
        // check against the denominator, stopping at the highest match
        while(factors.Count != 0) 
            // we have a common factor
            if (dm % (int)factors.Peek() == 0) 
                int f = (int)factors.Pop();
                nm /= f;
                dm /= f;
                break;
            
            else factors.Pop();
        
        nm += (n * dm);
        nm *= sign;
        if (dm == 1) return Convert.ToString(nm);
        else return Convert.ToString(nm) + "/" + Convert.ToString(dm);
    

【讨论】:

【参考方案20】:

重复小数的简单解决方案/细分。

我认为数字 1-9 除以 9 是重复的。又名 7/9 = .77777

我的解决方案是将整数乘以 9,加上重复的数字,然后再除以 9。

    Ex: 28.66666
    28*9=252
    252+6=258
    258/9=28.66666

这种方法也很容易编程。截断十进制数字,乘以 9,添加第一个小数,然后除以 9。

唯一缺少的是,如果左数可以被 3 整除,分数可能需要简化。

【讨论】:

【参考方案21】:

以下是这个问题的两个流行答案的 Swift 4 转换:

public func decimalToFraction(_ d: Double) -> (Int, Int) 
    var df: Double = 1
    var top: Int = 1
    var bot: Int = 1

    while df != d 
        if df < d 
            top += 1
         else 
            bot += 1
            top = Int(d * bot)
        
        df = top / bot
    
    return (top, bot)


public func realToFraction(_ value: Double, accuracy: Double = 0.00005) -> (Int, Int)? 
    var value = value
    guard accuracy >= 0 && accuracy <= 1 else 
        Swift.print(accuracy, "Must be > 0 and < 1.")
        return nil
    
    let theSign = sign(value)
    if theSign == -1 
        value = abs(value)
    

    // Accuracy is the maximum relative error; convert to absolute maxError
    let maxError = theSign == 0 ? accuracy : value * accuracy

    let n = floor(value)
    value -= n

    if value < maxError 
        return (Int(theSign * n), 1)
    

    if 1 - maxError < value 
        return (Int(theSign * (n + 1)), 1)
    

    // The lower fraction is 0/1
    var lowerN: Double = 0
    var lowerD: Double = 1

    // The upper fraction is 1/1
    var upperN: Double = 1
    var upperD: Double = 1

    while true 
        // The middle fraction is (lowerN + upperN) / (lowerD + upperD)
        let middleN = lowerN + upperN
        let middleD = lowerD + upperD

        if middleD * (value + maxError) < middleN 
            // real + error < middle : middle is our new upper
            upperN = middleN
            upperD = middleD
         else if middleN < (value - maxError) * middleD 
            // middle < real - error : middle is our new lower
            lowerN = middleN
            lowerD = middleD
         else 
            // Middle is our best fraction
            return (Int(n * middleD + middleN * theSign), Int(middleD))
        
    

【讨论】:

【参考方案22】:

第一个函数获取分数字符串格式“1/2”,第二个函数查找上下部分的gcd(最大公约数)。

public static string DoubleToFraction(double num)
            
    if (Math.Round(num, 6) == Math.Round(num, 0))
        return Math.Round(num, 0).ToString();
    bool minus = (num < 0) ? true : false;
    int up;
    if (minus)
        up = (int)((Math.Round(num, 6) - 0.000001) * 362880);
    else
        up = (int)((Math.Round(num, 6) + 0.000001) * 362880);
    int down = 362880;
    int div = gcd(up, down);
    up /= div;
    down /= div;
    return up + "/" + down;

public static int gcd(int a, int b)

    if (b == 0)
        return Math.Abs(a);
    return gcd(b, a % b);

【讨论】:

【参考方案23】:

我已尝试扩展 btilly's answer 这些变化是: 如果您想以 fration 格式显示它,请更改 btilly's answer 的最后一个 else 部分。于是修改后的代码变成了:

def float_to_fraction (x, error=0.000001):
    n = int(math.floor(x))
    x -= n
    if x < error:
        return (n, 1)
    elif 1 - error < x:
        return (n+1, 1)
    # The lower fraction is 0/1
    lower_n = 0
    lower_d = 1
    # The upper fraction is 1/1
    upper_n = 1
    upper_d = 1
    while True:
        # The middle fraction is (lower_n + upper_n) / (lower_d + upper_d)
        middle_n = lower_n + upper_n
        middle_d = lower_d + upper_d
        # If x + error < middle
        if middle_d * (x + error) < middle_n:
            # middle is our new upper
            upper_n = middle_n
            upper_d = middle_d
        # Else If middle < x - error
        elif middle_n < (x - error) * middle_d:
            # middle is our new lower
            lower_n = middle_n
            lower_d = middle_d
        # Else middle is our best fraction
        else:
            #return (n * middle_d + middle_n, middle_d)
            frac = Fraction(n * middle_d + middle_n, middle_d)
            if (frac.numerator // frac.denominator) == 0:
                return(f"frac.numerator % frac.denominator/frac.denominator")
            elif ((frac.numerator % frac.denominator)/frac.denominator) == 0/1:
                return(f"frac.numerator // frac.denominator")
            else:
                return(f"frac.numerator // frac.denominator "f"frac.numerator % frac.denominator/frac.denominator")```

【讨论】:

【参考方案24】:

这是 btilly 答案的 javascript 版本。我只是想将浮点数显示为分数,所以我返回一个字符串;

function float_to_fraction(x, error = 0.00001) 
 const n = Math.floor(x);
 x -= n;

 if (x < error) 
   return `$n`;
  else if (1 - error < x) 
   return `$n + 1`;
 

 //  The lower fraction is 0/1
 let lower_n = 0;
 let lower_d = 1;

 // The upper fraction is 1/1
 let upper_n = 1;
 let upper_d = 1;
 while (true) 
   // The middle fraction is (lower_n + upper_n) / (lower_d + upper_d)
   let middle_n = lower_n + upper_n;
   let middle_d = lower_d + upper_d;
   // If x + error < middle
   if (middle_d * (x + error) < middle_n) 
     // middle is our new upper
     upper_n = middle_n;
     upper_d = middle_d;
     // Else If middle < x - error
    else if (middle_n < (x - error) * middle_d) 
     // middle is our new lower
     lower_n = middle_n;
     lower_d = middle_d;
     //Else middle is our best fraction
    else 
     return `$n * middle_d + middle_n/$middle_d`;
   
 

【讨论】:

【参考方案25】:

我知道这是一篇旧帖子,但想分享我的想法。

public static string ToFraction(this decimal value)
    
        decimal numerator = value;
        int denominator = 0;

        while (numerator % 1 != 0)
        
            denominator++;
            numerator = value * denominator;                
        
        return decimal.ToInt32( numerator).ToString() + "/" + denominator.ToString();
    

【讨论】:

以上是关于将小数化简为分数的算法的主要内容,如果未能解决你的问题,请参考以下文章

分数求和

1.13.12

无限循环小数化分数

分数化简算法

无限循环小数化分数

[code] PTA 胡凡算法笔记 DAY043