如何从小数中获取分子和分母?

Posted

技术标签:

【中文标题】如何从小数中获取分子和分母?【英文标题】:how can i get numerator and denominator from a fractional number? 【发布时间】:2018-06-21 06:47:01 【问题描述】:

例如,从“1.375”我想得到“1375/1000”或“11/8”作为结果。我怎么能用 C++ 做到这一点? 我试图通过分隔点之前和之后的数字来做到这一点,但它不知道如何获得我想要的输出。

【问题讨论】:

你知道用铅笔和一张纸怎么做吗? 开始:1375/1000 = 1 和 375/1000(小数部分) 乘以 10,直到它不是整数得到 1375/1000 [然后将两个数字除以它们的 GCD 得到 11/8] 需要一点小心。许多看起来不错的分数数字并不能产生特别令人愉快的浮点数。见Is floating point math broken? 您想要const char *std::string 还是其他东西(doubleboost::rational<int>?)。您的源对象(字符串文字)"1.375" 或(双重文字)1.375 还是其中一种类型的变量? 【参考方案1】:

您并没有真正指定是否需要将浮点数或字符串转换为比率,所以我将假设前者。

您可以直接使用IEEE-754 编码的属性,而不是尝试基于字符串或算术的方法。

浮点数(按标准称为binary32)在内存中编码如下:

 S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM
 ^                                ^
bit 31                           bit 0

其中S 是符号位,Es 是指数位(其中 8 个)Ms 是尾数位(23 位)。

数字可以这样解码:

value = (-1)^S * significand * 2 ^ expoenent

where:
    significand = 1.MMMMMMMMMMMMMMMMMMMMMMM (as binary)
    exponent = EEEEEEEE (as binary) - 127

(注意:这是所谓的“正常数”,还有零、次正规、无穷大和 NaN - 请参阅我链接的 Wikipedia 页面)

这里可以使用。我们可以把上面的等式改写成这样:

(-1)^S * significand * exponent = (-1)^s * (significand * 2^23) * 2 ^ (exponent - 23)

重点是significand * 2^23 是一个整数(等于1.MMMMMMMMMMMMMMMMMMMMMMM,二进制 - 通过乘以 2^23,我们将点向右移动了 23 位)。2 ^ (exponent - 23) 显然也是一个整数。

换句话说:我们可以将数字写成:

(significand * 2^23) / 2^(-(exponent - 23))    (when exponent - 23 < 0)
or
[(significand * 2^23) * 2^(exponent - 23)] / 1 (when exponent - 23 >= 0)

所以我们有分子和分母——直接来自数字的二进制表示。


以上所有内容都可以在 C++ 中这样实现:

struct Ratio

    int64_t numerator; // numerator includes sign
    uint64_t denominator;

    float toFloat() const
    
        return static_cast<float>(numerator) / denominator;
    

    static Ratio fromFloat(float v)
    
        // First, obtain bitwise representation of the value
        const uint32_t bitwiseRepr = *reinterpret_cast<uint32_t*>(&v);

        // Extract sign, exponent and mantissa bits (as stored in memory) for convenience:
        const uint32_t signBit = bitwiseRepr >> 31u;
        const uint32_t expBits = (bitwiseRepr >> 23u) & 0xffu; // 8 bits set
        const uint32_t mntsBits = bitwiseRepr & 0x7fffffu; // 23 bits set

        // Handle some special cases:
        if(expBits == 0 && mntsBits == 0)
        
            // special case: +0 and -0
            return 0, 1;
        
        else if(expBits == 255u && mntsBits == 0)
        
            // special case: +inf, -inf
            // Let's agree that infinity is always represented as 1/0 in Ratio 
            return signBit ? -1 : 1, 0;
        
        else if(expBits == 255u)
        
            // special case: nan
            // Let's agree, that if we get NaN, we returns max int64_t by 0
            return std::numeric_limits<int64_t>::max(), 0;
        

        // mask lowest 23 bits (mantissa)
        uint32_t significand = (1u << 23u) | mntsBits;

        const int64_t signFactor = signBit ? -1 : 1;

        const int32_t exp = expBits - 127 - 23;

        if(exp < 0)
        
            return signFactor * static_cast<int64_t>(significand), 1u << static_cast<uint32_t>(-exp);
        
        else
        
            return signFactor * static_cast<int64_t>(significand * (1u << static_cast<uint32_t>(exp))), 1;
        
    
;

(希望上面的 cmets 和描述可以理解 - 如果有需要改进的地方,请告诉我)

为简单起见,我省略了对超出范围值的检查。

我们可以这样使用它:

float fv = 1.375f;
Ratio rv = Ratio::fromFloat(fv);
std::cout << "fv = " << fv << ", rv = " << rv << ", rv.toFloat() = " << rv.toFloat() << "\n";

输出是:

fv = 1.375, rv = 11534336/8388608, rv.toFloat() = 1.375

如您所见,两端的值完全相同。


问题在于分子和分母都很大。这是因为代码总是将有效位乘以 2^23,即使较小的值足以使其成为整数(这相当于将 0.2 写为 2000000/10000000 而不是 2/10 - 这是同一件事,只是写法不同) .

这可以通过将代码更改为将有效数乘以(并除以指数)乘以最小数字来解决,如下所示(省略号代表与上述相同的部分):

// counts number of subsequent least significant bits equal to 0
// example: for 1001000 (binary) returns 3
uint32_t countTrailingZeroes(uint32_t v)

    uint32_t counter = 0;

    while(counter < 32 && (v & 1u) == 0)
    
        v >>= 1u;
        ++counter;
    

    return counter;


struct Ratio

    ...

    static Ratio fromFloat(float v)
    
        ... 

        uint32_t significand = (1u << 23u) | mntsBits;

        const uint32_t nTrailingZeroes = countTrailingZeroes(significand);
        significand >>= nTrailingZeroes;

        const int64_t signFactor = signBit ? -1 : 1;

        const int32_t exp = expBits - 127 - 23 + nTrailingZeroes;

        if(exp < 0)
        
            return signFactor * static_cast<int64_t>(significand), 1u << static_cast<uint32_t>(-exp);
        
        else
        
            return signFactor * static_cast<int64_t>(significand * (1u << static_cast<uint32_t>(exp))), 1;
        
    
;

现在,对于以下代码:

float fv = 1.375f;
Ratio rv = Ratio::fromFloat(fv);

std::cout << "fv = " << fv << ", rv = " << rv << ", rv.toFloat() = " << rv.toFloat() << "\n";

我们得到:

fv = 1.375,rv = 11/8,rv.toFloat() = 1.375

【讨论】:

【参考方案2】:

在 C++ 中,您可以使用 Boost 理性类。但是你需要给出分子和分母。

为此,您需要找出输入字符串中小数点后的数字。您可以通过字符串操作函数来做到这一点。逐个字符读取输入的字符,发现.后面没有字符

char inputstr[30]; 
int noint=0, nodec=0;
char intstr[30], dec[30];
int decimalfound = 0;
int denominator = 1;
int numerator;

scanf("%s",inputstr);

len = strlen(inputstr);

for (int i=0; i<len; i++)

    if (decimalfound ==0)
    
        if (inputstr[i] == '.')
        
            decimalfound = 1;
        
        else
        
            intstr[noint++] = inputstr[i];
        
     
     else
     
        dec[nodec++] = inputstr[i];
        denominator *=10;
     

dec[nodec] = '\0';
intstr[noint] = '\0';

numerator = atoi(dec) + (atoi(intstr) * 1000);

// You can now use the numerator and denominator as the fraction, 
// either in the Rational class or you can find gcd and divide by 
// gcd.

【讨论】:

您没有输入验证。如果用户输入的长度超过您为inputstr 分配的大小,您可能会遇到分段错误。并且没有检查以确保输入是有效的十进制值。 decimalfound 需要初始化为0denominator1(不是10)。有其他人实际测试过这段代码吗? @DavidCollins - 我已经制作了一个基本代码,而不是所有检查的东西。所以没有添加长度检查和输入有效性检查。我已经解决了您提到的decimalfounddenominator 的问题。【参考方案3】:

这个简单的代码怎么样:

double n = 1.375;
int num = 1, den = 1;
double frac = (num * 1.f / den);
double margin = 0.000001;
while (abs(frac - n) > margin)
    if (frac > n)
        den++;
    
    else
        num++;
    
    frac = (num * 1.f / den);

我并没有真正测试太多,这只是一个想法。

【讨论】:

【参考方案4】:

我希望我发布一个使用“仅C 语言”的答案会被原谅。我知道你用C++ 标记了这个问题 - 但我无法拒绝诱饵,抱歉。这仍然是有效的 C++ 至少(尽管它确实主要使用 C 字符串处理技术)。

int num_string_float_to_rat(char *input, long *num, long *den) 
    char *tok = NULL, *end = NULL;
    char buf[128] = '\0';
    long a = 0, b = 0;
    int den_power = 1;

    strncpy(buf, input, sizeof(buf) - 1);

    tok = strtok(buf, ".");
    if (!tok) return 1;
    a = strtol(tok, &end, 10);
    if (*end != '\0') return 2;
    tok = strtok(NULL, ".");
    if (!tok) return 1;
    den_power = strlen(tok);  // Denominator power of 10
    b = strtol(tok, &end, 10);
    if (*end != '\0') return 2;

    *den = static_cast<int>(pow(10.00, den_power));
    *num = a * *den + b;

    num_simple_fraction(num, den);

    return 0;

示例用法:

int rc = num_string_float_to_rat("0015.0235", &num, &den);
// Check return code -> should be 0!
printf("%ld/%ld\n", num, den);

输出:

30047/2000

http://codepad.org/CFQQEZkc 上的完整示例。

注意事项:

strtok() 用于将输入解析为令牌(在这方面无需重新发明***)。 strtok() 修改其输入 - 因此使用临时缓冲区以确保安全 它检查无效字符 - 如果找到,将返回一个非零返回码 已使用 strtol() 代替 atoi() - 因为它可以检测输入中的非数字字符 scanf() 没有被用于 slurp 输入 - 由于浮点数的舍入问题 strtol() 的基数已明确设置为 10,以避免前导零出现问题(否则前导零会导致数字被解释为八进制) 它使用 num_simple_fraction() 帮助器(未显示) - 反过来又使用 gcd() 帮助器(也未显示) - 将结果转换为简单分数 分子的log10()是通过计算小数点后token的长度来确定的

【讨论】:

【参考方案5】:

我会分三步完成。

1) 找到小数点,这样你就知道分母有多大了。

2) 得到分子。就是去掉了小数点的原文。

3) 得到分母。如果没有小数点,则分母为 1。否则,分母为 10^n,其中 n 是(现已删除的)小数点右侧的位数。

struct fraction 
    std::string num, den;
;

fraction parse(std::string input) 
    // 1:
    std::size_t dec_point = input.find('.');

    // 2:
    if (dec_point == std::string::npos)
        dec_point = 0;
    else 
        dec_point = input.length() - dec_point;
        input.erase(input.begin() + dec_point);
    

    // 3:
    int denom = 1;
    for (int i = 1; i < dec_point; ++i)
        denom *= 10;
    string result =  input, std::to_string(denom) ;
    return result;

【讨论】:

以上是关于如何从小数中获取分子和分母?的主要内容,如果未能解决你的问题,请参考以下文章

166 Fraction to Recurring Decimal 分数到小数

2021-10-20:分数到小数。给定两个整数,分别表示分数的分子numerator和分母denominator,以字符串形式返回小数。如果小数部分为循环小数,则将循环的部分括在括号内。输入: num

怎么把小数化成分数的方法

小数怎么转化分数?

java怎么把小数转为分母为一位数的分数

每日一题592. 分数加减运算