为什么 ((unsigned int)low + (unsigned int)high)) >> 1 有效?

时间:2021-05-18 09:31:17

标签: c algorithm binary-search

我不明白这两者是什么关系

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

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

为什么它们能以防止溢出问题的方式按预期工作?我认为转换为无符号类型会破坏结果,但似乎不会。从数学上讲,我无法解释它们为什么起作用。

这个问题实际上与二分查找有关。 the renown bug 直到 2006 年才被检测到。

enter image description here

6 个答案:

答案 0 :(得分:3)

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

这仅在以下情况下“有效”:(1)您限制自己使用 int 索引,并且(2)您的 C 编译器实现了(实际上是通用的,但实际上并非由语言标准保证) ) 允许 unsigned 类型重新利用符号位来表示更多正值的行为:在这种情况下,unsigned 类型的最大可表示正值是其相应有符号类型的两倍多一点(您实际上得到额外的位使用,因为不需要存储符号)。这已经足够了,因为您只添加了 2 个 int,每个最多可以是 INT_MAX2 * INT_MAX < UINT_MAX。如果您改为使用 unsigned 索引并遇到 INT_MAX 上方的一对索引,则此技术将溢出。

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

总是有效,无论类型如何。 (例如,如果您将 int 更改为 unsigned,它将继续适用于 unsignedhigh 的所有 low 值。)那是因为如果我们暂时想象一下 int 可以表示任何整数,它在数学上等同于通常的书写方式:

int mid = (low + high) / 2

如果我们有足够的位,两个表达式将计算相同的值,而前者永远不会用完位,因为中间表达式 (high - low, (high - low) / 2, low + (high - low) / 2) 大于 high,我们已经知道它可以在 int 中表示。

答案 1 :(得分:2)

第一个版本没有完全解决问题。

您链接的文章做出了一些没有明确指出的假设。 在显示的代码 low 中,midhigh 都是类型为 int 的签名类型。 此外,由于它们用作数组的索引,因此只有正值才有效。

将我们限制为正值,转换为 unsigned int 根本不会改变值。它仅允许我们使用 MSB,以防发生 int 溢出。对于无符号整数,最高位并不意味着负值,当我们将其移位 1 时,符号和值与我们预期的一样。

如果没有这两个约束,您的代码将无法运行。一旦 lowhigh 已经是无符号整数,您可能会再次遇到相同的溢出问题。 (我知道,C 标准不会将其称为无符号值的溢出,但这并不能解决问题。)在这种情况下,溢出位不会出现在变量中,并且在移位后会得到错误的值。

无论如何,第二个版本确实解决了这个问题: 如果您减去 2 个都在 0..INT_MAX 范围内的数字,您也会得到该范围内的结果。 (鉴于从较大的减去较小的。) 从基础数学我们知道 low+mid 也必须在这个范围内,因为 mid 小于 high。 所以我们不会在这里遇到溢出问题。

答案 2 :(得分:2)

两者都通过确保不超出允许范围来工作,并利用两个值都不能为负的事实。

后者很简单:它使用减法来确保值永远不会超过 high 的值。

前者使用不同的技术:它通过增加允许范围来绕过问题。如果将两个 N 位数字相加,则结果最多为 N+1 位。我们使用没有符号位的无符号数获得了额外的位。

请注意,位移 (>> 1) 不提供任何额外值;您也可以使用除法 (/ 2),因为数字是无符号的。

答案 3 :(得分:2)

它们(仅)有效,因为已知 lowhigh 至少为 0。

((unsigned int)low + (unsigned int)high)) >> 1

正有符号整数总是小于相应类型的最大无符号整数的一半,因为 unsigned 获得了一个额外的范围位。如果 lowhigh 可能为负数,则无符号加法中可能存在“溢出”1

low + (high - low) / 2

high - low 中永远不会出现下溢,因为只有当 high 为负时才会出现下溢。永远不会有溢出,因为只有在 low 为负时才会发生。 result / 2 永远不会溢出或下溢,您总是会得到一个接近于 0 的值。将它加回到 low 永远不会溢出,因为结果永远不会超过 high

  1. 无符号算术不会溢出,因为它被定义为模 2N,其中 N 是位数。但是,您最终得到了一个包装值,用作索引仍然不正确

答案 4 :(得分:1)

什么时候可能溢出?如果 lowhigh 都接近 INT_MAX

为简单起见考虑字节大小。

如果我们将 0x7D0x7F125127)相加,我们得到 0xFC = 0b11111100

如果我们将此值解释为有符号,则有 -4,如果我们将此值解释为无符号,则有 252

有符号右移作为算术移位SAR,用符号位填充左边部分,给出0b11111110,有符号-2(注意与整数除以2的结果相同)。我们绝对不希望 125127 的平均值出现这样的结果。

无符号右移作为移位SHR,给出0b01111110,无符号126

所以第一个表达式在无符号算术中工作,直到最后的赋值,当结果已经在 INT_MAX 的范围内时

答案 5 :(得分:0)

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

不起作用。最后的 ) 太多了。

我不确定此处 high 的转换。但编译器礼貌地建议

<块引用>

警告:建议在“>>”内的“+”周围加上括号

9 |     int mid = (unsigned int) low + high >> 1;
  |               ~~~~~~~~~~~~~~~~~~~^~~~~~

这给出了 ((...) 模式 ant 而不是 ((...))。有或没有第二次演员。


使用未签名的强制转换和所需的括号:

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

这是一个小技巧(OP 2nd version):

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

“从低到高的距离的一半”实际上是概念,而不是“平均”。这个额外的操作确实是一笔不错的投资。

但这有一个微妙的(?!)错误:

int mid = low/2 + high/2;  //If both uneven --> 1 lost

int 应该是无符号且更大的:size_tunsigned long。或者只是long:那么我们可以在 20 年后再次谈论它。


1988 年的 K&R C 书在他们自己的 qsort 中采用 int 时也有同样的错误。库之一在原型中有 size_t 。仅当有人试图对比 SIZE_MAX 大一半的字符数组进行排序时,这将因草率的平均公式而失败 - 这很容易就是 ULONG_MAX。