【问题标题】:Does this number have more than D decimal places?这个数字的小数点多于 D 位吗?
【发布时间】:2010-09-22 00:03:10
【问题描述】:

假设我想编写一个执行以下操作的函数:

给定一个数字 N,
当 N 舍入为 D1 位时
结果是否包含超过 D2 位小数,不包括尾随零?

例如,假设 N 是 0.01001,D1 是 4,D2 是 2。问题就变成了,.0100 是否包含超过 2 个小数位,不包括尾随零?答案是“不”。但如果 N 是 0.00101,答案将是“是”。

考虑到浮点数的限制,我正在寻找一种使用标准 C 库函数执行此操作的有效方法。

我的预期用法示例:如有必要,使用四位数字显示数字,否则使用两位数字显示。

(这不是作业题——这是作为一名专业程序员的结果,他在学生时代没有做这些作业。)

【问题讨论】:

  • 这其实是一个有趣又难的问题。如果是作业,我想知道导师会寻找什么样的答案。我能想到的唯一正确答案就是我给 CS 学生的答案。

标签: c floating-point


【解决方案1】:

使用标准库执行此操作的唯一简单方法是使用正确格式的snprintf(或只是sprintf),然后自己数零。正确计算(二进制)浮点数的十进制表示是一项非常非常困难的任务,您不想尝试自己做,而且您编写比标准库更快的正确版本的机会几乎为零.

我希望我做对了;未经测试:

double n; /* the number N */
int d1, d2; /* the parameters d1 and d2 */
char s[MAXLEN], *z;
snprintf(s, sizeof s, "%.*f", d1, n);
for (z=s+strlen(s)-1; *z==0; z--);
if (strlen(++z)<d1-d2) puts("yes");
else puts("no");

编辑: 正如 ssianky 所指出的,snprintf 可能对其打印的精度有限制。实际上,只要实现文档本身,C 标准几乎允许任何浮点运算无缘无故地给出错误结果,但是鼓励 IEEE 行为,并且 POSIX 还要求正确舍入到 DECIMAL_DIG 位的结果,但是允许实现在打印足够多的位置以唯一确定实际浮点值之后打印废话(例如全零)。所以长话短说,如果d1 相当大,或者如果您的平台是病态的,基于snprintf 的方法可能无法给出正确的答案。 (实际上,他们会在 GNU 系统上给出正确答案,而在 Microsoft 系统上给出错误答案。)

如果您关心这个缺点并希望得到d1 的任何值的正确结果,则必须使用implement exact float-to-decimal code yourself。涉及将浮点值重复乘以 10 的循环是不够的。

编辑 2:查看 OP 的“预期用途”,使用 snprintf 似乎很容易。如果您想打印开头的值并且只是想决定要使用多少小数位,只需将其打印到一个字符串中,然后在显示它之前去掉尾随的零。事实上,%gprintf 格式说明符甚至可以做 OP 想要做的事情......

【讨论】:

  • 谢谢——我很喜欢阅读答案和讨论。我期待解决方案是一个数学解决方案,但我认为你是对的,砍掉零是最好的方法。
【解决方案2】:

您可以先将您的数字乘以 10^D1 并将其四舍五入到最接近的整数,然后检查它是否可被 10^D2 整除。有一些舍入函数可供选择,它们将向上/向下舍入/远离零等,因此请务必检查您是否使用了您想要的。

下面的函数被编写成很小且独立的,但生产实现应该使用十次方的查找表,如注释行中所示。这将产生更快的性能并避免在浮点乘积中累积错误。

int f (double N, unsigned int D1, unsigned int D2)
{
   int i, n_mult_round, ten_d2;
   /* instead of the loop below, a real implementation should use */
   /* N *= dbl_powers_of_ten[D1]; */
   /* where dbl_powers_of_ten is a double array containing powers of ten */
   for (i = 0; i < D1; ++i) {
      N *= 10.0;
   }
   n_mult_round = (int) round (N);
   /* instead of the loop below, a real implementation should use */
   /* ten_d2 = int_powers_of_ten[D2]; */
   /* where int_powers_of_ten is an int array containing powers of ten */
   ten_d2 = 1;
   for (i = 0; i < D2; ++i) {
      ten_d2 *= 10;
   }
   if ( n_mult_round % ten_d2 == 0 ) {
      return ( 1 );
   }
   else {
      return ( 0 );
   }
}

【讨论】:

  • 乘以 10 的幂是浮点数的有损运算 - 乘以 10^n 会破坏 2*n 位的精度。这很可能会导致错误的结果。
  • @R.. 你确定吗?这意味着将 32 位浮点数乘以 10^16 会产生完整的垃圾。
  • @Philip:试试(1.0+DBL_EPSILON)*10.0 - 确保将结果存储到内存并回读,这样您就不会获得过高的精度。你应该得到10.0。然后尝试将一些数字(例如 1.0000000123)乘以 10 的大幂,看看会发生什么。
  • @R.. 他没有争论乘法是否会导致精度损失。但是,您对损失“率”的说法是荒谬的!乘以 10 的幂主要影响指数。完全有任何损失的唯一原因是尾数在 base-10 和 base-2 之间的不精确转换。
  • +1 考虑到问题的意图(决定 2 对 4 十进制表示),这个解决方案应该是完全可以接受的。
【解决方案3】:
  1. 将其格式化为字符串以达到最大精度。
  2. 从右边删除零,直到达到最小精度。

这里有一个不安全的实现来帮助您入门:

char buff[20]; /* make as big as necessary */
int maxPrecision = 4;
int minPrecision = 2;

sprintf(buff, "%.*f", maxPrecision, myFloat);

char *p = buff + (strlen(buff) - 1);
char *punct = strchr(buff, ".");

/* remove trailing zeros until we reach minPrecision */
while (*p == '0' && p > (punct + minPrecision))
    *p-- = 0; 

【讨论】:

  • 不要在运行时为此构造格式字符串。 RTFM 关于printf 格式字符串中的* 字符。
  • @R: 没必要用语言...毕竟答案没有错,只是次优。
  • 对不起,坏习惯。 RTFM 只是我对 RTM 的表示法。 :-)
【解决方案4】:

这个呢? fmod 应该返回指定小数位数之后的小数,所以我们可以比较这两个数字,看看它们是否相等。

int needsD1Decimals(float N, int D1, int D2) {
    float pow1 = pow(0.1f, D1); // 0.0001
    float pow2 = pow(0.1f, D2); // 0.01
    float mod1 = fmod(N, pow1); // 0.00001
    float mod2 = fmod(N, pow2); // 0.00001
    if (fabs(mod1 - mod2) > (pow1 / 2)) { // > 0.00005 to handle errors
        return 1;
    }
    return 0;
}

如果您只是想打印出正确的答案,那么只打印最多小数位并在末尾截断零可能会更快:

void printNumber(float N, int D1, int D2) {
    char format[256];
    char result[256];
    sprintf(format, "%%.%df", D1);
    sprintf(result, format, N);
    char *end = result + strlen(result) - 1;
    int zeros = 0;
    while (end > result && end[0] == '0' && zeros < (D1 - D2))
    {
        zeros++;
        end--;
    }
    if (zeros >= (D1 - D2))
    {
        end[1] = '\0';
    }
    puts(result);
}

void doNumber(float N, int D1, int D2) {
    printf("%f, %d, %d: ", N, D1, D2);
    printNumber(N, D1, D2);
    printf("\n");
}

int _tmain(int argc, _TCHAR* argv[])
{
    doNumber(0.01001f, 4, 2);
    doNumber(0.010101f, 4, 2);
    doNumber(0.011001f, 4, 2);
    doNumber(5000.0f, 4, 2);
    return 0;
}

【讨论】:

    【解决方案5】:
    int foo(float number, int d1, int d2)
    {
        assert((number > 0.0f) && (number < 1.0f) && (d1 <= FLT_DIG) && (d2 < d1) && (d2 > 0));
    
        int count = d1;
        int n = (int)(number * pow(10.0f, d1));
    
        if (n == 0) return 0;
    
        while ((n % 10) == 0)
        {
            n /= 10;
            count--;
        }
    
        return (count > d2) ? 1 : 0;
    }
    

    【讨论】:

    • -1 表示无可救药的错误答案和对浮点的无知
    • 如果我可以为编写甚至无效的代码给出另一个-1 C:int(number)...???
    • 你的程序输出什么? (一旦你修复了语法错误......)它在我的机器上给出了 25。提示:您完全忽略了问题陈述中的参数d1。
    • 1) 我觉得你完全不知道浮点数。乘以 10 不会改变小数部分,而只会改变指数。是的,int(n) 不是有效的 C 转换,但 (int)n 是。
    • @R.. : int(number) 是有效的 C++。它与 C 的 (int) number 转换具有相同的语义。但是,短暂地使用错误的语言是这个答案的最小问题。
    猜你喜欢
    • 1970-01-01
    • 2013-02-08
    • 1970-01-01
    • 2021-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多