【问题标题】:Explanation of the safe average of two numbers解释两个数字的安全平均值
【发布时间】:2013-10-07 00:46:33
【问题描述】:

每当我需要对二分搜索等算法的两个数字进行平均时,我总是会这样做:

int mid = low + ((high - low) / 2);

我最近在this post 看到了另一种方法,但我不明白。它说你可以在 Java 中做到这一点:

int mid = (low + high) >>> 1;

或者 C++ 中的这个:

int mid = ((unsigned int)low + (unsigned int)high)) >> 1;

C++ 版本本质上使两个操作数都无符号,因此进行移位会导致算术移位而不是有符号移位。我了解这两段代码在做什么,但这如何解决溢出问题?我认为整个问题是中间值high + low 可能溢出?

编辑:

哦,呵呵。所有的答案都没有完全回答我的问题,但正是@John Zeringue 的回答让它点击了。我会在这里解释一下。

Java 中(high + low)/2 的问题并不完全是high + low 溢出(它确实溢出,因为整数都是有符号的,但所有位仍然存在,并且没有信息丢失)。像这样取平均值的问题是除法。除法是对有符号值进行运算,因此您的结果将为负数。使用移位代替将除以二,但考虑位而不是符号(有效地将其视为无符号)。

【问题讨论】:

  • Java 没有无符号整数。考虑一下如果你的int 溢出会发生什么?还要考虑>>> 的作用。
  • 我建议你尝试一下,看看它有什么不同。几十年来,这一直是一个已知问题,因此您可以查找它,但您将通过自己弄清楚它来了解更多信息。

标签: java c++ math bit-manipulation integer-overflow


【解决方案1】:

您看到的代码已损坏:它无法正确计算负数的平均值。如果您仅对非负值(如索引)进行操作,那没关系,但这不是一般的替代品。 你原来的代码,

int mid = low + ((high - low) / 2);

也不安全,因为high - low 的差异可能会溢出有符号整数的范围。同样,如果您只使用非负整数,那也没关系。

利用A+B = 2*(A&B) + A^B 的事实,我们可以计算两个整数的平均值而不会像这样溢出:

int mid = (high&low) + (high^low)/2;

您可以使用位移来计算除以 2,但请记住,两者并不相同:除法向 0 舍入,而位移始终向下舍入。

int mid = (high&low) + ((high^low)>>1);

【讨论】:

  • 如果希望 avg(-x,y) 等于 -avg(x,y),则基于除法的方法更好。如果希望 avg(x+n,y+n) 等于 avg(x,y)+n,则基于移位的除法可能会更好。
  • @Joni 公式很漂亮!
【解决方案2】:

所以让我们考虑字节而不是整数。唯一的区别是一个字节是一个 8 位整数,而一个 int 有 32 位。在 Java 中,两者总是有符号的,这意味着前导位表示它们是正数 (0) 还是负数 (1)。

byte low = Byte.valueOf("01111111", 2); // The maximum byte value
byte high = low; // This copies low.

byte sum = low + high; // The bit representation of this is 11111110, which, having a
                       // leading 1, is negative. Consider this the worst case
                       // overflow, since low and high can't be any larger.

byte mid = sum >>> 1; // This correctly gives us 01111111, fixing the overflow.

对于整数,也是一样的。基本上所有这些的要点是,在有符号整数上使用无符号位移位允许您利用前导位来处理最大可能的低值和高值。

【讨论】:

  • 很好的解释。但如果你能原谅我的无礼,如果两个数字都是负数呢?从您的示例中可以看出,最后的位移会/可能会丢失符号位。
  • 没错。对于负数,您必须使用带符号的位移位 >>。您可以使用 if-else 语句轻松处理这种情况。此方法唯一不起作用的数字是(对于字节)-128 和 -128。对于其他整数类型,它始终是最小值加上自身。如果你很好奇,我建议你自己玩。不过我不会使用字节。原来Java不支持真正的字节加法,所以我上面给出的代码在实践中是行不通的。
  • 请注意,最初的问题是关于平均指数,在这种情况下负数不是问题。
【解决方案3】:

C++ 版本有一个隐藏的作弊:lowhighints,但它们从不否定。当您将它们转换为 unsigned int 时,您的符号位将成为一个额外的精度位,单次加法不会溢出。

这不是一个很好的作弊方法,因为数组索引无论如何都应该是unsigned

就像在其他地方所说的那样,i >> 1 表示/2 用于无符号整数。

【讨论】:

    【解决方案4】:

    C++ 版本没有解决溢出问题。它只解决了使用 shift 而不是 / 成功除以 2 的问题,如果可以提高性能,您的编译器应该能够自行进行优化。

    另一方面,如果您的整数类型足够大以容纳合理范围的索引,则溢出可能不是真正的问题。

    【讨论】:

      【解决方案5】:

      您不能在 Java 中使用 unsigned int。如果发生溢出,则考虑低 32 位,丢弃高位。无符号右移将帮助您将 int 视为无符号 int。但是,在 C++ 中,您不会出现溢出。

      【讨论】:

        【解决方案6】:

        通过使用您所说的已经使用的方式,您可以避免整数溢出,即:

        int mid = low + ((high - low) / 2);
        

        如果需要的话,让你的编译器来优化它。

        【讨论】:

        • 其他代码比这更具可读性,并且两者的速度可能相同。我的问题是它为什么有效......
        • 右边加法溢出怎么办?如果 high 是 INT_MAX 并且 low 是负数,那么加法将溢出 => 未定义的行为。
        • @Phil 这并不是真正的未定义行为,至少在 java 中是这样。很清楚会发生什么,但在这种情况下答案将是错误的事实是另一回事。
        • 这两种语言都不起作用。在 C++ 中,int 溢出是未定义的行为,并且现有编译器的优化会破坏假设加法执行 mod 2^n 的代码。 Java说“结果是数学和的低位,以一些足够大的二进制补码格式表示。如果发生溢出,则结果的符号与两者的数学和的符号不同操作数值。”我尝试了 high = Integer.MAX_VALUE 和 low = -1,上面的代码在我的 Java 上给出了一个负值,这显然不是中点。 @尤金
        • @Phil 你自己说过,与 C++ 不同,Java 确实有一个可预测且易于描述的结果。这就是我的全部观点。通常用于显示(a+b)/2 中断的示例是a=b=Integer.MAX_VALUE
        【解决方案7】:

        程序综合技术似乎可以解决这些问题。

        在这个视频中,程序员指定了约束 a) 没有溢出,b) 没有除法,c) 没有 if-then-else。合成器自动生成了一些不错的东西。

        https://youtu.be/jZ-mMprVVBU

        【讨论】:

          猜你喜欢
          • 2020-09-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-10
          • 1970-01-01
          • 2013-08-29
          • 2017-02-21
          相关资源
          最近更新 更多