如何从小数中获取分子和分母?
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
还是其他东西(double
或boost::rational<int>
?)。您的源对象(字符串文字)"1.375"
或(双重文字)1.375
还是其中一种类型的变量?
【参考方案1】:
您并没有真正指定是否需要将浮点数或字符串转换为比率,所以我将假设前者。
您可以直接使用IEEE-754 编码的属性,而不是尝试基于字符串或算术的方法。
浮点数(按标准称为binary32
)在内存中编码如下:
S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM
^ ^
bit 31 bit 0
其中S
是符号位,E
s 是指数位(其中 8 个)M
s 是尾数位(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
需要初始化为0
和denominator
为1
(不是10
)。有其他人实际测试过这段代码吗?
@DavidCollins - 我已经制作了一个基本代码,而不是所有检查的东西。所以没有添加长度检查和输入有效性检查。我已经解决了您提到的decimalfound
和denominator
的问题。【参考方案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