为什么log(1000)/log(10)不同于log10(1000)?

5

今天,我遇到了一个相当奇怪的问题。我需要计算数字的字符串长度,所以我想出了这个解决方案:

// say the number is 1000
(int)(log(1000)/log(10)) + 1

这是基于数学公式的。

log10x = lognx/logn10(在这里有解释)。

但我发现,在C中:

(int)(log(1000)/log(10)) + 1

不等于NOT

(int) log10(1000) + 1

但事实并非如此。 我甚至使用以下Java代码尝试了同样的事情。
(int) (Math.log(1000) / Math.log(10)) + 1
(int) Math.log10(1000) + 1

但它表现出相同错误的方式。

故事继续。在执行这段代码之后

for (int i = 10; i < 10000000; i *= 10) {
   System.out.println(((int) (Math.log10(i)) + 1) + 
                " " + ((int) (Math.log(i) / Math.log(10)) + 1));
}

我明白了

2 2
3 3
4 3  // here second method produces wrong result for 1000
5 5
6 6
7 6  // here again

这个错误似乎出现在1000的倍数上。

我向我的C语言老师展示了这个问题,他说这可能是由于对数除法过程中的某种类型转换错误引起的,但他不知道原因。

所以我的问题是:

  • 为什么 (int) (Math.log(1000) / Math.log(10)) + 1 不等于 (int) Math.log10(1000) + 1,尽管根据数学应该相等。
  • 为什么只有1000的倍数才有问题?

编辑:这不是四舍五入误差,因为

Math.floor(Math.log10(i)) + 1
Math.floor(Math.log(i) / Math.log(10)) + 1

产生相同的错误输出
2 2
3 3
4 3
5 5
6 6
7 6

编辑2:我必须向下取整,因为我想知道数字的位数

log10(999) + 1 = 3.9995654882259823
log10(1000) + 1 =  4.0

如果我只是四舍五入,那么得到的结果(4)是错误的,因为999有3个数字。


2
这些的未舍入输出为: log10(1000) = 4.0 log(1000)/log(10) = 3.9999999999999996使用“floor”而不是“round”的理由是什么? - pjp
1
尝试计算log10(999) + 1,你会得到3.9995654882259823,但需要向下取整,因为我想要数字的位数。如果我四舍五入,会得到4,这与log10(1000) + 1相同,但它多了一位数字。 - Jakub Arnold
2
最好使用 Integer.toString(n).length() 来获取长度。 - starblue
5
你有一位编程老师,他不理解有限精度。这是一所糟糕的大学。 - erikkallen
查看此问题及其相关源代码,以使用BigDecimals计算对数 https://dev59.com/3XRB5IYBdhLWcg3wCjjO - pjp
显示剩余2条评论
8个回答

23

您提供了代码片段

for (int i = 10; i < 10000000; i *= 10) {
   System.out.println(((int) (Math.log10(i)) + 1) + 
                " " + ((int) (Math.log(i) / Math.log(10)) + 1));
}

为了说明您的问题,只需删除对 int 的强制转换并再次运行循环即可。您将会收到

2.0 2.0
3.0 3.0
4.0 3.9999999999999996
5.0 5.0
6.0 6.0
7.0 6.999999999999999

这个回答已经直接回答了你的问题。正如tliff所说,强制转换会截断小数而不是正确舍入。

编辑: 你更新了你的问题,使用 floor(), 但是像强制转换一样,floor() 会舍去小数并向下取整!


如果我不强制转换并执行Math.log10(9) + 1的话,它会得出类似于1.954的结果,而正确答案应该是1,因为2是Math.log10(10) + 1的结果。 - Jakub Arnold
gs:不,4有一个精确的浮点表示。错误来自除法之前的不精确值。 - caf
为什么这被标记为正确答案?你是否找到了如何区分问题中的log10(999)和log10(1000)的方法? - Aurelien Ribon

8
日志操作是一种超越函数。计算机评估结果的最佳方法是使用逼近所需操作的代数函数。结果的准确性取决于计算机使用的算法(这可能是FPU中的微码)。在英特尔FPU上,有影响各种超越函数(三角函数也是超越函数)精度的设置,FPU规格将详细说明所使用算法的准确级别。
因此,除了上述提到的舍入误差之外,还存在准确性问题,因为计算出的log(x)可能不等于实际的log(x)。

5
这是由于精度和舍入问题导致的。 Math.log(1000) / Math.log(10) 不等于 3。
如果需要精确度,请不要使用浮点算术 - 并且一般放弃对数。 浮点数固有模糊性。 要获得精确结果,请使用整数算术。
我真的建议您一般不要走这条路,但听起来您正在取对整数以确定某个数量级。 如果是这种情况,则 (int)(Math.log(x+0.5) / Math.log(10)) 更稳定 - 但请注意,double 只有53位精度,因此大约在第15个双倍体时,不能准确地表示整数,那么这个技巧就行不通了。

4

将一个非常小的值加到分子上,以避免 Skizz 指出的精度问题。

// say the number is 1000
(int)((log(1000)+1E-14)/log(10)) + 1

1E-14应该足以将准确性推回正轨。

将小值从1E-15更改为1E-14,这将导致某些输入产生错误的结果

我对一组随机的unsigned long long进行了1E-14的测试,所有数字都通过了测试。


1
1E-15太小了,对于“1000”无效...最好使用上面建议的0.5。 - user85421
你说得没错...但是0.5表明你正在尝试解决一个四舍五入的问题。1E-14(使用IEEE 64位双精度)适用于1000,至少在我测试的0到2^64-1之间的几个值上有效。 - pmg

2

更新:这是由于精度和四舍五入误差导致的。


没关系,因为它使用公式将一个进制转换为另一个进制。 - Jakub Arnold

0

如果你想要把结果作为整数呈现,你应该进行四舍五入而不仅是简单地截取小数点后的部分。

你可能得到了类似6.999999的数字并将其向下舍入为6。


我实际上不能四舍五入,因为当你有log10(100)和log10(99)时,第二个数字略低于2,所以当我四舍五入时,它会给我与100相同的结果,这是错误的..不过我可以使用floor()函数向下取整 - Jakub Arnold

0
使用(int)强制转换,您正在截断必要的小数部分。尝试不进行强制转换将它们作为双精度打印(反正您为什么要进行强制转换?),那么一切都会好的。

0

打印出中间结果,即log(1000),log(10),log(1000)/log(10)和log10(1000)。这比猜测更有帮助。


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