C/C++ 如何计算小数位数?

22
假设用户输入的是一个小数,例如5.2155(有4位小数)。它可以自由地存储(int,double等)。
有没有聪明(或非常简单)的方法来找出这个数字有多少位小数呢?(有点像如何通过屏蔽最后一位来确定一个数字是偶数还是奇数的问题)。

你能澄清一下“数字有多少位小数”吗?你是指数字的小数部分有多少位小数位数吗? - Doug
是的,你的假设是正确的。 - Milan
16个回答

21

我知道两种方法,但都不是很聪明,这更多地是环境的限制,而不是我的过错 :-)

第一种是使用 "%.50f" 格式字符串将数字转换为大缓冲区,并删除尾随零,然后计算小数点后的字符数。这将受到 printf 系列本身的限制。或者您可以使用用户输入的字符串作为输入(而不是 sprintf 浮点值),以完全避免浮点问题。

第二种方法是减去整数部分,然后迭代乘以10,并再次减去整数部分,直到得到零。这被计算机表示浮点数的限制所限制-在每个阶段,您可能会出现无法精确表示的数字(因此.2155实际上可能是.215499999998)。像下面这样(未经测试,除了在我的脑海中,这与 COMX-35 差不多):

count = 0
num = abs(num)
num = num - int(num)
while num != 0:
    num = num * 10
    count = count + 1
    num = num - int(num)

如果你知道待排序的数字类型(例如,它们都是小数点后0到4位的数字),你可以使用标准的浮点数“技巧”来正确地排序。例如,不要使用如下方式:

while num != 0:

使用

while abs(num) >= 0.0000001:

1
.2155 不会变成 .21559999999999,但是 .2156 可能会。 - quant_dev
1
没有人喜欢一个自以为是的家伙 :-) 谢谢 @quant_dev,我已经修复了它。 - paxdiablo
2
我认为你很聪明,因为你回答问题非常全面。 - ojblass
3
“while abs(num) <= 0.0000001” 应该改为 “while abs(num) >= 0.0000001” 吗? - Guillaume Algis
2
@Guillaume,是的,恭喜你成为四年来第一个发现那个错误的人。根据你的建议已经修复了。 - paxdiablo
显示剩余3条评论

8
一旦将数字从用户表示(字符串、OCR-ed gif 文件等)转换为浮点数,您不一定处理相同的数字。因此,严格而言,答案是“否”。
如果(情况A),您可以避免将数字从字符串表示转换,则问题变得更加容易,您只需要计算小数点后的数字并减去尾随零的数量。
如果您无法做到这一点(情况B),则需要对最大小数位数进行假设,将数字转换回字符串表示,并使用round-to-even method将其舍入到此最大数字。例如,如果用户提供了1.1,它被表示为1.09999999999999(假设),将其转换回字符串会得到“1.09999999999999”。将该数字四舍五入到四个小数点,得到“1.1000”。现在又回到了情况A。

7

就我个人而言:

从小数部分开始:0.2155

反复乘以10并丢弃数字的整数部分,直到得到零。步骤的数量将是小数的位数。例如:

.2155 * 10 = 2.155
.155 * 10 = 1.55
.55 * 10 = 5.5
.5 * 10 = 5.0

4步 = 4位小数


这个很容易产生舍入误差。试一下吧。 - Bim

3

类似这样的东西也可能适用:

float i = 5.2154;
std::string s;
std::string t;
std::stringstream out;
out << i;
s = out.str();

t = s.substr(s.find(".")+1);
cout<<"number of decimal places: " << t.length();

1
你需要考虑那些不容易放入浮点数中的数字,例如如果你得到了5.215399999999999,你可能想要报告4位小数。 - Mike Kale

2
使用科学计数法格式(以避免舍入误差):
#include <stdio.h>
#include <string.h>

/* Counting the number of decimals
 *
 * 1. Use Scientific Notation format
 * 2. Convert it to a string
 * 3. Tokenize it on the exp sign, discard the base part
 * 4. convert the second token back to number
*/

int main(){

   int counts;
   char *sign;
   char str[15];
   char *base;
   char *exp10;
   float real = 0.00001;

   sprintf (str, "%E",  real);
   sign= ( strpbrk ( str, "+"))? "+" : "-";

   base = strtok (str, sign);
   exp10 = strtok (NULL, sign);

   counts=atoi(exp10);

   printf("[%d]\n", counts);

   return 0;
}

[5]


2
你说的“stored freely (int)”是什么意思?一旦存储在int中,它就没有小数点了,这是显而易见的。double以二进制形式存储,因此与“小数”之间没有明显或简单的关系。为什么不将输入保留为字符串,只需足够长以计算那些小数点,然后将其发送到最终的数字变量目标?

1
多年之后,我已经用三行代码找到了自己的解决方案:
string number = "543.014";    
size_t dotFound;
stoi(number, &dotFound));
string(number).substr(dotFound).size()

当然,在此之前,您需要测试它是否真的是浮点数(例如使用stof(number) == stoi(number))。

1
如果您的数字的小数部分存储在单独的int中,您可以计算它的小数位数。这是对Andrei Alexandrescu改进的改进。他的版本已经比朴素的方法快(在每个数字处除以10)。下面的版本是常数时间,至少在x86-64和ARM上对所有大小都更快,但它占用的二进制代码是原来的两倍,因此它不太友好缓存。此版本针对unsigned而非signed。请参考我在Facebook Folly上的PR进行基准测试。
inline uint32_t digits10(uint64_t v) {
  return  1
        + (std::uint32_t)(v>=10)
        + (std::uint32_t)(v>=100)
        + (std::uint32_t)(v>=1000)
        + (std::uint32_t)(v>=10000)
        + (std::uint32_t)(v>=100000)
        + (std::uint32_t)(v>=1000000)
        + (std::uint32_t)(v>=10000000)
        + (std::uint32_t)(v>=100000000)
        + (std::uint32_t)(v>=1000000000)
        + (std::uint32_t)(v>=10000000000ull)
        + (std::uint32_t)(v>=100000000000ull)
        + (std::uint32_t)(v>=1000000000000ull)
        + (std::uint32_t)(v>=10000000000000ull)
        + (std::uint32_t)(v>=100000000000000ull)
        + (std::uint32_t)(v>=1000000000000000ull)
        + (std::uint32_t)(v>=10000000000000000ull)
        + (std::uint32_t)(v>=100000000000000000ull)
        + (std::uint32_t)(v>=1000000000000000000ull)
        + (std::uint32_t)(v>=10000000000000000000ull);
}

3
这个函数计算整数中数字的数量,而不是问题所要求的十进制数字的数量。 - keith

1
这是一个强大的C++ 11实现,适用于float和double类型:

template <typename T>
std::enable_if_t<(std::is_floating_point<T>::value), std::size_t>
decimal_places(T v)
{
    std::size_t count = 0;

    v = std::abs(v);

    auto c = v - std::floor(v);

    T factor = 10;

    T eps = std::numeric_limits<T>::epsilon() * c;

    while ((c > eps && c < (1 - eps)) && count < std::numeric_limits<T>::max_digits10)
    {
        c = v * factor;

        c = c - std::floor(c);
            
        factor *= 10;

        eps = std::numeric_limits<T>::epsilon() * v * factor;

        count++;
    }

    return count;
}

每次迭代都会抛弃值,而是跟踪10的幂乘积以避免累积舍入问题。它使用机器ε(epsilon)来正确处理不能在二进制中精确表示的十进制数,例如问题中规定的5.2155的值。


对于问题中给出的示例“5.2155”,并使用IEEE-754二进制64位算术运算来处理double类型,此代码返回6。 - Eric Postpischil
@Eric Postpischil,谢谢,我已经更新了我的答案。 - keith

0

根据其他人的写作,这对我很有效。这个解决方案确实处理了数字无法在二进制中精确表示的情况。

如其他人建议的那样,while循环的条件指示了精确的行为。我的更新使用机器epsilon值来测试任何循环上的余数是否可以由数字格式表示。测试不应与0或硬编码值(例如0.000001)进行比较。

template<class T, std::enable_if_t<std::is_floating_point_v<T>, T>* = nullptr>
unsigned int NumDecimalPlaces(T val)
{
    unsigned int decimalPlaces = 0;
    val = std::abs(val);
    val = val - std::round(val);
    while (
        val - std::numeric_limits<T>::epsilon() > std::numeric_limits<T>::epsilon() && 
        decimalPlaces <= std::numeric_limits<T>::digits10)
    {
        std::cout << val << ", ";

        val = val * 10;
        ++decimalPlaces;
        val = val - std::round(val);
    }
    return val;
}

例如,如果输入值为2.1,则正确的解决方案是1。然而,这里发布的一些其他答案会在使用双精度时输出16,因为2.1无法在双精度中精确表示。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接