使用整数除法进行四舍五入
有没有一种简单、符合Python风格的方法,可以在不使用浮点数的情况下,四舍五入到最接近的整数?我想用整数运算来实现以下功能:
skip = int(round(1.0 * total / surplus))
==============
@John:浮点数在不同平台上可能会有不一样的表现。如果你希望你的代码在不同的平台上都能通过测试,就需要避免使用浮点数(或者在测试中加一些复杂的处理,期望它能正常工作)。上面的代码可能在大多数或所有平台上都能正常工作,但我觉得还是避免使用浮点数更简单。这样做有什么“违背Python精神”的呢?
7 个回答
简而言之:
q, r = divmod(total, surplus)
skip = q if q % 2 == 0 and 2*r == surplus else q + (2*r // surplus) # rounds to nearest integer; half to nearest even
这个解决方案:
- 按照提问者的要求,四舍五入到最近的整数
- 适用于正整数和负整数
- 将0.5的分数部分四舍五入到最近的偶数(例如,
rnd(0.5)=0
,rnd(1.5)=2
),这和提问者使用的Python的round
函数的行为一致 - 遵循整数运算,符合提问者的要求(可以查看
divmod
的文档)
详细内容
受到zhmyh的回答的启发,我想出了以下解决方案(更新:这个方案只适用于非负的总数和剩余量,评论中有提到):
q, r = divmod(total, surplus)
skip = q + int(bool(r)) # rounds to next greater integer (always ceiling)
因为提问者要求四舍五入到最近的整数,所以zhmyh的方案实际上有点不对,因为它总是向下一个更大的整数四舍五入,而我的方案符合要求只适用于非负的总数和剩余量。一个正确的方案,能够处理负数的是:
q, r = divmod(total, surplus)
skip = q + int(2 * r >= surplus) # rounds to nearest integer (floor or ceiling) if total and surplus are non-negative
不过请注意,如果2 * r == surplus
,它基本上会对正负结果都进行向上取整,比如ceil(-1.5) == -1
,而ceil(1.5) == 2
。在四舍五入到最近整数的情况下,这种行为是正确的(因为到下一个更低和更高整数的距离相等),但在零的参考上是不对称的。为了修正这个问题,也就是将0.5向远离零的方向四舍五入,我们可以添加一个布尔条件:
q, r = divmod(total, surplus)
skip = q + (2 * r // surplus) # rounds to nearest integer (positive: half away from zero, negative: half toward zero)
更进一步,为了像提问者使用的Python的round
函数那样,将0.5四舍五入到最近的偶数:
q, r = divmod(total, surplus)
skip = q if q < 0 and 2*r == surplus else q + (2*r // surplus) # rounds to nearest integer; half away from zero
如果你想知道divmod
是如何定义的:根据它的文档
对于整数,结果与
(a // b, a % b)
相同。
因此,我们坚持使用整数运算,符合提问者的要求。
skip = (((total << 1) // surplus) + 1) >> 1
把数字向左移动一位,相当于把它乘以2;把数字向右移动一位,则是把它除以2,并且结果会向下取整。如果在中间加上1,那么如果结果的小数部分超过0.5,就会向上取整。
这其实和你写的...
skip = int((1.0*total/surplus) + 0.5)
是一样的,只不过所有的数先乘以2,然后再除以2。这种操作可以用整数运算来完成,因为位移操作不需要浮点数。
你可以很简单地做到这一点:
(n + d // 2) // d
,这里的 n
是被除数,d
是除数。
还有其他方法,比如 (((n << 1) // d) + 1) >> 1
或者等价的 (((n * 2) // d) + 1) // 2
,但在最近的 CPython 版本中,这些方法可能会更慢,因为 int
的实现方式像以前的 long
。
简单的方法只需要访问 3 次变量,加载 1 次常量,并进行 3 次整数运算。而复杂的方法需要访问 2 次变量,加载 3 次常量,并进行 4 次整数运算。整数运算的耗时可能会根据数字的大小而变化。访问函数内部的局部变量不需要“查找”。
如果你真的很急着追求速度,可以做一些基准测试。否则,保持简单就好。