为什么在Java中 (high + low) / 2 是错误的,但是 (high + low) >>> 1 不是?

31
我理解>>>修复了溢出问题:当两个较大的正长整数相加时,可能会得到一个负数。有人能解释一下这个位移操作是如何神奇地解决了溢出问题?以及它与>>有何不同吗?
我的猜想是:我认为这与Java使用二进制补码有关,因此如果我们有额外的空间,则溢出将是正确的数字,但由于我们没有足够的空间,所以结果变成了负数。因此,当您进行位移并用零填充时,由于使用了二进制补码,问题就神奇地解决了。但我可能错了,需要有位运算的脑子的人来确认一下。 :)

1
我猜你是在指二分查找的那个bug - user541686
1
你怎么知道它修复了溢出问题?还有,什么溢出问题? - user334856
1
Joshua Bloch 告诉我:http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html - JohnPristine
3个回答

55

简而言之,(high + low) >>> 1 是一个技巧,利用未使用的符号位对非负数进行正确的平均计算。


在假设 highlow 都为非负数的情况下,我们可以确定最高位(符号位)为零。

因此,highlow 实际上都是31位整数。

high = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824
low  = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824

当你将它们加在一起时,它们可能会“溢出”到最高位。

high + low =       1000 0000 0000 0000 0000 0000 0000 0000
           =  2147483648 as unsigned 32-bit integer
           = -2147483648 as signed   32-bit integer

(high + low) / 2   = 1100 0000 0000 0000 0000 0000 0000 0000 = -1073741824
(high + low) >>> 1 = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824
  • 作为一个32位有符号整数,它会溢出并变成负数。因此(high + low) / 2是错误的,因为high + low可能是负数。

  • 作为32位无符号整数,求和是正确的,只需要将其除以2。

当然,Java不支持无符号整数,所以我们最好的方法是用逻辑右移运算符>>>来将其除以2(作为无符号整数)。

在具有无符号整数的语言中(如C和C++),情况会更加棘手,因为输入可以是完整的32位整数。一种解决方案是:low + ((high - low) / 2)


最后列举一下>>>>>/之间的区别:

  • >>>是逻辑右移。它使用0填充上面的位。
  • >>是算术右移。它使用原始顶部位的副本填充上面的位。
  • /是除法。

从数学角度来看:

  • x >>> 1x视为无符号整数,并将其除以2。它向下舍入。
  • x >> 1x视为有符号整数,并将其除以2。它向负无穷大取整。
  • x / 2x视为有符号整数,并将其除以2。它向零取整。

不确定何时使用 >>> 和何时使用 >>。我认为在这种情况下,我们选择 >>> 来解决可能的溢出(符号位溢出)问题。选择 >> 和 >>> 之间的经验法则是什么? - JohnPristine
10
>>> 是一个逻辑右移位运算符,它会用零填充高位。而 >> 是一个算术右移位运算符,它会用原始数值的最高位进行扩展以填充高位。在数学上,>>> 1 将数字视为无符号数并向下取整除于二,而 >> 1 将数字视为有符号数并向负无穷方向向下取整。而 / 2 则将数字视为有符号数并向零取整。 - Mysticial
不错,答案永远不会过时。 - Soner from The Ottoman Empire

11

它使用零填充最高位,而不是使用符号位填充它们。

int a = 0x40000000;
(a + a)  /  2 == 0xC0000000;
(a + a) >>> 1 == 0x40000000;

1
@JohnPristine:嗯,二进制补码的CPU对于有符号和无符号整数执行加法的方式完全相同...唯一的区别在于 / 2。所以在这一点上,一切都是正确的。显然,由于有足够的位来表示和,也有足够的位来表示商,而 >>> 1 就是这样做的。/ 2 是错误的唯一原因是它试图修正符号,而 >>> 1 执行位右移,这与无符号除以2相同...因此它 必须 正确工作。我不确定这是否回答了你的问题... - user541686
我的陈述正确吗:“我认为这与Java使用二进制补码有关,因此如果我们有额外的空间,溢出就是正确的数字,但由于我们没有,它变成了负数。所以当你用零进行移位和填充时,它会神奇地得到修复。”? - JohnPristine

5

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