让Java的模数运算在负数情况下表现正常的最佳方法是什么?

135

在Java中,当你执行以下操作时:

a % b
如果a是负数,它将返回一个负结果,而不是像应该一样回绕到b。修复这个问题的最佳方法是什么?我唯一能想到的方法是什么。
如果a是负数,则结果将变为负数,而不是像应该一样绕到b。如何修复这个问题?我所能想到的唯一方法是:
a < 0 ? b + a : a % b

14
在处理负数时,没有一种“正确”的模数行为方式——很多语言是这样做的,很多语言是不同的,有几种语言则完全不同。至少前两种方法都有其优缺点。 - user395760
5
这对我来说很奇怪。我认为只有在 b 为负数时才会返回负数。 - fent
1
可能是Java如何处理负数取模运算?的重复问题。 - Erick Robertson
2
这是正确的。但是那个问题的标题应该改名。如果我正在搜索这个问题,我不会点击那个问题,因为我已经知道Java的取模运算如何工作了。 - fent
7
我只是把它从“为什么-13%64 = 51?”这个标题改成了现在这个,因为那个标题永远不可能成为人们搜索的内容。所以这个问题标题更好,而且更容易被搜索到关键词,比如余数、负数、计算和数字等。 - Erick Robertson
1
Java 没有模数运算符。许多程序员错误地称为模数的运算符实际上被称为余数运算符,这也暗示了它为什么会表现出这样的行为。 - StackOverthrow
7个回答

171
它的表现是符合预期的:a % b = a - a / b * b,也就是余数。
你可以使用(a % b + b) % b

这个表达式的作用是,(a % b) 的结果一定小于 b,无论 a 是正数还是负数。添加 b 可以处理 a 的负值,因为 (a % b) 是介于 -b0 之间的负值,(a % b + b) 一定小于 b 并且为正数。最后一个模数是为了防止 a 最初就是正数,因为如果 a 是正数,则 (a % b + b) 将变得大于 b。因此,(a % b + b) % b 再次将其转换为小于 b(并不影响负的 a 值)。


3
谢谢,这样效果更好。并且它也适用于比b大得多的负数。 - fent
6
由于(a % b)的结果一定小于b(无论a是正数还是负数),加上b可以处理a的负值,因为(a % b)小于b且小于0,所以(a % b + b)一定小于b并且为正数。最后一个模数运算是为了处理a最初是正数的情况,因为如果a是正数,则(a % b + b)会变大超过b。因此,(a % b + b) % b将其再次变成小于b的值(不影响负的a值)。 - ethanfar
2
@eitanfar,我已将您出色的解释包含在答案中(对于 a < 0 进行了轻微更正,也许您可以看一下)。 - Maarten Bodewes
7
我刚刚在另一个关于相同主题的问题中看到了这个评论: 值得一提的是,当a和b非常大时,(a%b + b)%b会失效。 例如,使用 a = Integer.MAX_VALUE - 1b = Integer.MAX_VALUE将会得到 -3 作为结果,这是一个负数,而这正是您想要避免的。 - Thorbear
2
@Mikepote 如果你真的需要,使用 while 会更慢,除非你只需要一个 if,在这种情况下它实际上会更快。 - Peter Lawrey
显示剩余7条评论

127

3
Java 8+ 的最佳答案。 - Charney Kaye
很酷,我不知道那个。Java 8 明确修复了一些烦人的问题。 - Franz D.
6
不错的方式。但不幸的是,它不能用于floatdouble参数。 取模二元运算符(%)也适用于floatdouble操作数。 - Mir-Ismaili
在某种程度上,更好的问题是如何在Java中执行“xyz”数学运算。让事情变得复杂的是人们相信只有一种正确的方法来做这件事。 - Chris Mountford

16

对于那些尚未使用(或无法使用)Java 8的人,Guava通过IntMath.mod()在Guava 11.0版本中提供了救济。

IntMath.mod( 2, 3) = 2
IntMath.mod(-2, 3) = 1

需要注意的是:与Java 8的Math.floorMod()不同,除数(第二个参数)不能为负数。


10
在数论中,结果总是为正数。我猜在计算机语言中并不总是这样,因为不是所有程序员都是数学家。我的看法是,我会认为这是语言的设计缺陷,但现在无法更改。
=MOD(-4,180)= 176 =MOD(176,180)= 176
因为180 *(-1)+ 176 = -4与180 * 0 + 176 = 176相同
使用时钟示例,在这里http://mathworld.wolfram.com/Congruence.html,您不会说时间持续时间mod周期长度为-45分钟,您会说15分钟,即使两个答案都满足基本方程。

1
在数论中,它并不总是正的... 它们属于同余类。您可以自由选择该类中的任何候选项来进行符号表示,但是其映射到该类的所有内容都是相同的。如果使用该类中的特定其他候选项可以显著简化某个问题(例如选择-1而不是n-1),那么就可以使用它。 - BeUndead
1
这绝对是一个自以为比其他人都更懂的“大牌”程序员,只会给所有人带来混乱。本应该使用不同于%的运算符来计算余数,因为传统上余数的返回值在0和模数之间。这很可能会导致难以发现的错误,例如飞机越过赤道时翻转等问题。请注意,我并不建议现在进行更改。 - Richard Thomas
@Richard Thomas:同意。关于飞机翻转的问题:使用四元数可以避免这一切,并使代码更加清晰(在概念上和编程上)。 - Jérôme JEAN-CHARLES

5
Java 8有一个Math.floorMod,但它非常慢(它的实现有多个除法、乘法和条件语句)。然而,JVM可能有一个内在的优化存根来加速它。如果没有floorMod,最快的方法就像这里其他答案所示,但没有条件分支,只有一个缓慢的%运算符。假设n是正数,x可以是任何值:
int remainder = (x % n); // may be negative if x is negative
//if remainder is negative, adds n, otherwise adds 0
return ((remainder >> 31) & n) + remainder;

n = 3 时,结果为:
x | result
----------
-4| 2
-3| 0
-2| 1
-1| 2
 0| 0
 1| 1
 2| 2
 3| 0
 4| 1

如果你只需要在 0n-1 之间进行均匀分布,而不需要确切的模运算,并且你的 x 不会靠近 0,那么以下方法会更快,因为有更多的指令级并行性,在计算慢速的 % 时,其他部分可以和它并行执行,因为它们不依赖于其结果。 return ((x >> 31) & (n - 1)) + (x % n) 以下是 n=3 时上述代码的结果:
x | result
----------
-5| 0
-4| 1
-3| 2
-2| 0
-1| 1
 0| 0
 1| 1
 2| 2
 3| 0
 4| 1
 5| 2

如果输入是int类型的完整范围内的随机数,那么两个解决方案的分布将相同。如果输入接近零,则后一个解决方案中在 n - 1 处会有太少的结果。


3
似乎假设int为32位。这可能大多数情况下是安全的,但Java网站说:“int:默认情况下,int数据类型是32位有符号二进制补码整数。”“默认情况下”听起来有点不可靠。[编辑:进一步挖掘,看起来规范显示了坚实的32位,所以继续] - Richard Thomas

2

这里有一个替代方案:

a < 0 ? b-1 - (-a-1) % b : a % b

这个公式可能比那个公式[(a % b + b) % b]快,也可能不快。与另一个公式不同的是,它包含一个分支,但使用了一个更少的模运算。如果计算机可以正确地预测a < 0,则可能会胜出。
(编辑:修正了公式。)

1
但是模数运算需要进行除法,这可能会更慢(特别是如果处理器几乎总是正确猜测分支)。因此,这样做可能更好。 - dave
@KarstenR。你是正确的!我修正了公式,现在它可以正常工作(但需要再减去两个数)。 - Stefan Reich
那是真的,@dave。 - Stefan Reich

0

使用floorMod方法是最好的选择。

我很惊讶没有人提到显而易见的事。

Math.abs(a) % b

3
这是错误的。先取绝对值会改变模数:floorMod(-1, 4) ==> 3,但 Math.abs(-1) % 4 ==> 1 - just-max

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