一种小数转分数的算法(不限整除)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;
}
  • 13
    点赞
  • 4
    评论
  • 31
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

11076 浮点数的分数表达 时间限制:1000MS 内存限制:1000K 提交次数:0 通过次数:0 题型: 编程题 语言: 无限制 Description 在计算机中,用float或double来存储小数有时不能得到精确值,若要精确表达一个浮点数的计算结果, 最好用分数来表示小数,有限小数或无限循环小数都可以分数,无限循环小数的循环节用括号标记出来。如: 0.9 = 9/10 0.(3) = 0.3(3) = 0.3(33) = 1/3 当然一个小数可以用好几种分数形式来表示,我们只感兴趣最简的分数形式(即分母最小),如: 0.3(33) = 1/3 = 3/9 因为任何一个数都可以为一个整数和一个纯小数之和,整数部分较为简单无需做额外处理,只要将纯小数部分分数形式,整数部分的分数部分就很简单了。 现在给定一个正的纯小数(这个纯小数为有限小数或无限循环小数),请你以最简分数形式来返回这个纯小数。 输入格式 给定一个纯小数,若是无限循环小数,用括号标记循环节,输入小数表达不超过100个字符。 输出格式 输出:为最简分数形式,分子在前,分母在后,中间空格连接。 输入样例 0.3(33) 输出样例 1 3 提示 此题涉及如下几个问题: 一、字符串输入的问题 此题采用字符串接收输入,大家在接受数据的时候,不要用(c=getchar())!='\n'诸如此类一个字符一个字符接受, 然后判断是否是回车符号来结束输入,这样的方式在你本机运行不会有问题,但OJ系统中会有错误,无法输出结果, 因为OJ的测试平台行末并非'\n'字符。这里接受数据用scanf的%s,或cin等,会自动判别结束字符的,你就不要在你程序里专门去判别或吸收回车字符。 char a[105]; scanf("%s",a); 或cin >> a; 二、高精度或64位整数表示的问题 此题题目规定:输入小数表达不超过100个字符。 如此长的数,本意要大家用高精度数的运算来求解. 但后台测试数据没有做如此之长,放松一些吧,用64位整数也是允许通过的! 实现上,所有分子分母的变量,以及求最大公约数,都须用64位整数。 编译环境不同,对64位整数的定义和输入输出略有不同: 1)gnu gcc/g++ 中long long类型,或unsigned long long, 输入输出用cin和cout直接输出,用scanf和printf也可以的。 long long a; cin >> a; cout << a; 也可以使用:(注意一下,本OJ系统的gcc/g++不支持64位整数以"%I64d"形式输出, 但标准gnu gcc是支持如下的,在codeblocks上可以无误运行) scanf("%I64d",&a); printf("%I64d",a); 2)vc中用__int64类型,或unsigned __int64 scanf("%I64d",&a); printf("%I64d",a); vc下,64整数不要用cin和cout来输入输出,据说vc下64位整数兼容不好,会出错!大家可测试一下如下程序在vc下是否会出错? __int64 a; cin >> a; cout << a; 三、本题的解题思路 考虑输入的是纯小数,先暂时不考虑分子和分母有公因子的情况。 (1) 假设有限小数:X = 0.a1a2…an,式中的a1,a2,…,an都是0~9的数字。 X = 0.a1a2…an = a1a2…an/10^n (2) 假设无限循环小数:X = 0.a1a2…an(b1b2…bm),式中的a1,a2,…,an, b1,b2,…,bm都是0~9的数字,括号为循环节。 第一步,先将X为只有循环部分的纯小数。 X = 0.a1a2…an(b1b2…bm) (10^n)*X = a1a2…an + 0.(b1b2…bm) X = (a1a2…an + 0.(b1b2…bm)) / (10^n) 上式中,a1a2…an是整数部分,容易解决。重点考虑小数部分0.(b1b2…bm)如何分数形式,再加上整数部分即可。 第二步,考虑Y = 0.(b1b2…bm),将Y分数, (10^m)*Y = b1b2…bm + 0.(b1b2…bm) ((10^m)-1)*Y = b1b2…bm Y = b1b2…bm / ((10^m)-1) 将第二步的Y带入第一步的X,可得: X = (a1a2…an+Y)/(10^n) = ((a1a2…an)*((10^m)-1) + (b1b2…bm)) / ((10^m)-1)*(10^n) 此时,可以将任何一个有限小数或无限循环小数分数表示,分数的分子和分母如上分析的公式。 但此时的分子分母未必是最简的,对分子分母再进行约分, 删去公共的因子,A/B = (A/GCD(A,B))/(B/GCD(A,B)),为简单形式。 作者 zhengchan --------------------------------------------------------------------------------
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值