一种小数转分数的算法(不限整除)C++

最近需要用到小数转分数算法,便研究了一下。

先看一下最终程序的效果:
最终效果图

说一下数学中有理小数转分数的过程:
有理小数分为有限小数和无限循环小数

1. 有限小数:

有限小数直接去小数点再约分即可。

例: 1.55=155100=3120

2. 无限循环小数:

先判断循环节长度 n ,原小数a乘以 10n 后再减去 a 可化为有限小数。

例:1.8123123...

a=1.8123123...

循环节为123,一共三位,乘以 103

103a=1812.3123123...

两式相减可得 999a=1810.5

a=1810.5999=181059990=1207666

刚开始想使用上述方法,于是做了一些实验。
计算器算了分数,123/321=0.38317757009345794392523364485981,可见,123、321这两个数并不是很大,但是相除后得到的小数循环节过长,一个double类型尾数长度是52bit,对应十进制只有15~16位的精度,不能保证用算法分析出循环节的长度,上述方法不适用。下面使用连分数做转换。


使用连分数做转换

一、连分数的定义:

形如 a0+1a1+1a2+1a3+1... 的分式叫做连分式,记为 [a0;a1,a2,a3,...]
有理数(整数、有限小数、无限循环小数)的连分式是有限的。

二、有理数与连分数的转换:

1. 分数或小数转换为连分数

步骤1:令a=这个数,即a←这个数;
步骤2:将a的整数部分⌊a⌋记录下来;
步骤3:令a为a的小数部分,即a←a-⌊a⌋;
步骤4:如果a≠0,跳转到步骤2,否则向下执行步骤5;
步骤5:步骤2中记录的数即为连分数的 [a0;a1,a2,a3,...]
例如将 321123 2.6097560975... 转换为连分数,步骤如下表:

321123 2.6097560975...
321123=275123 2.6097560975...=2+0.6097560975
1/0.6097560975...=1.64
12375=14875 1.64=1+0.64
1/0.64=1.5625
7548=12748 1.5625=1+0.5625
1/0.5625=1.7777...
4827=12127 1.7777...=1+0.7777...
1/0.7777=1.285714285714...
2721=1621 1.285714285714...=1+0.285714285714...
1/0.285714285714...=3.5
216=336=312 3.5=3+0.5
1/0.5=2
21=2 2=2+0


可见 321123=[2;1,1,1,1,3,2]=2+11+11+11+11+13+12

2. 连分数转换为分数或小数

直接通分即可:如 [2;1,1,1,1,3,2]=10741

3. 利用上述方法,将有理数转为连分数再通分,可将 2.6097560975... 转为分数 10741

三、转换算法的C++程序

class ContinuedFraction
{
private:
    //用于保存连分数列表
    std::vector<unsigned> nums;
public:
    //小数转为连分数的构造函数
    ContinuedFraction(double a);
    //连分数转为小数
    double ToDouble();
};

ContinuedFraction::ContinuedFraction(double a)
{
    if (a < 1e-12)
    {
        nums.push_back(0);
        return;
    }
    for (;;)
    {
        unsigned tr = (unsigned)a;
        nums.push_back(tr);
        a -= tr;
        if (a < 1e-12) //小数部分是0, 退出
        {
            return;
        }
        a = 1 / a;
    }
}

double ContinuedFraction::ToDouble()
{
    double a = nums.back();
    for (auto it = nums.rbegin() + 1; it != nums.rend(); it++)
    {
        a = 1 / a;
        a += *it;
    }
    return a;
}

四、计算机转换时的问题

上述转换算法,我们需要做步骤3中的判断(a≠0)才能知道什么时候跳出循环,由于浮点型的误差,程序中a与0作比较应该这样写:abs(a - 0) < 1e-12,其中1e-12为很小的数,也就是说浮点型a与b比较相等,由于误差,需要写成a与b的差的绝对值是否充分小。
程序中的unsigned tr = (unsigned)a; a -= tr;功能是取a中的小数部分,初始浮点型为15位的有效数字,但减去整数部分后会引起浮点型的误差增大。
例如123.00123(8个有效数字)取小数后为0.00123(3个有效数字),有效数字的个数减少了5。
也就是说有效数字减少的个数为 整数的总位数3(123为3位)+小数点后零的个数2(0.00123有2个0)=5。
由于循环的次数,误差将越来越大,所以代码中应该计算实时的误差为多少,才能使a≠0的判断更有效。
程序如下

//初始浮点型的误差
double esp = 1e-15;
for (;;)
{
    unsigned tr = (unsigned)a;
    nums.push_back(tr);
    a -= tr;
    if (a < 1e-12) //小数部分是0, 直接退出
    {
        return;
    }
    //因a减整数部分而引起的浮点型误差增大
    //例如123.123,变为0.123,精度也由6位变为3位
    while (tr > 0)
    {
        tr /= 10;
        esp *= 10;
    }
    //因a取小数引起的浮点型误差增大
    //例如123.00123,变为0.00123,精度也由8位变为3位
    double t = a;
    while (t < 0.1)
    {
        t *= 10;
        esp *= 10;
    }
    a = 1 / a;
}

上面代码已经实时的计算出浮点数的误差esp,理论上在最后一句a = 1 / a;前面加上if (a < esp) return;即可。但是我们计算浮点数的误差过于严谨,可能导致还没有到0就提前退出循环了。所有这里我们把a < esp时候的插入nums的索引indexZheng记录下来,向后再多计算几次一直到2*indexZheng后再退出循环。
转换为分数时,可以分别将nums个数取indexZheng到2*indexZheng对应的double值与给定的double值比较,取误差最小对应的分数。


最终的程序如下:

ContinuedFraction.h

#pragma once
#include <vector>

class ContinuedFraction
{
private:
    //用于保存连分数列表
    std::vector<unsigned> nums;
    int indexZheng = -1;
public:
    //小数转为连分数的构造函数
    ContinuedFraction(double a);
    //连分数转为小数
    double ToDouble();
    friend class Fraction;
};

ContinuedFraction.cpp

#include "ContinuedFraction.h"

ContinuedFraction::ContinuedFraction(double a)
{
    if (a < 1e-12)
    {
        nums.push_back(0);
        return;
    }
    //初始浮点型的误差,经测试1e-12效果最好
    double esp = 1e-12;
    for (int i = 0; indexZheng == -1 || i <= 2 * (indexZheng + 5); i++)
    {
        unsigned tr = (unsigned)a;
        nums.push_back(tr);
        a -= tr;
        if (a < 1e-12) //小数部分是0, 直接退出
        {
            if (indexZheng == -1)
            {
                indexZheng = i;
            }
            return;
        }
        //因a减整数部分而引起的浮点型误差增大
        //例如123.123,变为0.123,精度也由6位变为3位
        while (tr > 0)
        {
            tr /= 10;
            esp *= 10;
        }
        因a取小数引起的浮点型误差增大
        例如123.00123,变为0.00123,精度也由8位变为3位
        //double t = a;
        //while (t < 0.1)
        //{
        //  t *= 10;
        //  esp *= 10;
        //}
        if (indexZheng == -1 && a < esp)
        {
            indexZheng = i;
        }
        a = 1 / a;
    }
}

double ContinuedFraction::ToDouble()
{
    double a = nums.back();
    for (auto it = nums.rbegin() + 1; it != nums.rend(); it++)
    {
        a = 1 / a;
        a += *it;
    }
    return a;
}

Fraction.h

#pragma once
#include "ContinuedFraction.h"

class Fraction
{
private:
    //符号位
    bool sign;
    //倒数
    void Invert();
public:
    //分子
    unsigned a;
    //分母
    unsigned b;
    Fraction() {}
    Fraction(double a);
    Fraction(ContinuedFraction confrac);
    void Print();
    double ToDouble();
};

Fraction.cpp

#include "Fraction.h"
#include <cmath>
#include <vector>
#include <iostream>

Fraction::Fraction(ContinuedFraction confrac) : b(1), sign(false)
{
    a = confrac.nums.back();
    for (auto it = confrac.nums.rbegin() + 1; it != confrac.nums.rend(); it++)
    {
        Invert();
        a += b * *it;
    }
}

Fraction::Fraction(double a)
{
    sign = a < 0;
    if (sign) a = -a;

    if (a < 1e-12)
    {
        this->a = 0;
        this->b = 1;
        this->sign = false;
        return;
    }
    ContinuedFraction confrac(a);
    std::vector<unsigned> nums = confrac.nums;

    int firstIndex = confrac.indexZheng - 4;
    firstIndex = firstIndex >= 0 ? firstIndex : 0;
    confrac.nums.resize(firstIndex);

    std::vector<Fraction> vfr;
    std::vector<double> vesp;
    //一次一次增加连分数精度,并计算误差
    for (size_t i = firstIndex; i < nums.size(); i++)
    {
        confrac.nums.push_back(nums[i]);
        Fraction fr(confrac);
        double d = fr.ToDouble();
        vfr.push_back(fr);
        vesp.push_back(abs(d - a));
    }
    //查找误差最小的index
    int mindex = 0;
    double m = 1e100;
    for (size_t i = 0; i < vesp.size(); i++)
    {
        if (vesp[i] < m)
        {
            mindex = i;
            m = vesp[i];
        }
    }
    this->a = vfr[mindex].a;
    this->b = vfr[mindex].b;
    return;
}

void Fraction::Invert()
{
    unsigned t = a;
    a = b;
    b = t;
}

void Fraction::Print()
{
    std::cout << (sign ? "-" : "") << a << "/" << b << std::endl;
}

double Fraction::ToDouble()
{
    double n = (double)a / b;
    return sign ? -n : n;
}

main.cpp

#include "Fraction.h"

int main()
{
    Fraction f1(46531463. / 34554517);
    f1.Print();

    Fraction f2(4.25);
    f2.Print();

    Fraction f3(-1.3333333333333);
    f3.Print();

    return 0;
}
  • 20
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值