我不明白这两者是什么关系
int mid = ((unsigned int)low + (unsigned int)high)) >> 1
和
int mid = low + (high - low) / 2
为什么它们能以防止溢出问题的方式按预期工作?我认为转换为无符号类型会破坏结果,但似乎不会。从数学上讲,我无法解释它们为什么起作用。
这个问题实际上与二分查找有关。 the renown bug 直到 2006 年才被检测到。
答案 0 :(得分:3)
int mid = ((unsigned int)low + (unsigned int)high)) >> 1
这仅在以下情况下“有效”:(1)您限制自己使用 int
索引,并且(2)您的 C 编译器实现了(实际上是通用的,但实际上并非由语言标准保证) ) 允许 unsigned
类型重新利用符号位来表示更多正值的行为:在这种情况下,unsigned
类型的最大可表示正值是其相应有符号类型的两倍多一点(您实际上得到额外的位使用,因为不需要存储符号)。这已经足够了,因为您只添加了 2 个 int
,每个最多可以是 INT_MAX
和 2 * INT_MAX < UINT_MAX
。如果您改为使用 unsigned
索引并遇到 INT_MAX
上方的一对索引,则此技术将溢出。
int mid = low + (high - low) / 2
这总是有效,无论类型如何。 (例如,如果您将 int
更改为 unsigned
,它将继续适用于 unsigned
和 high
的所有 low
值。)那是因为如果我们暂时想象一下 int
可以表示任何整数,它在数学上等同于通常的书写方式:
int mid = (low + high) / 2
如果我们有足够的位,两个表达式将计算相同的值,而前者永远不会用完位,因为中间表达式 (high - low
, (high - low) / 2
, low + (high - low) / 2
) 大于 high
,我们已经知道它可以在 int
中表示。
答案 1 :(得分:2)
第一个版本没有完全解决问题。
您链接的文章做出了一些没有明确指出的假设。
在显示的代码 low
中,mid
和 high
都是类型为 int
的签名类型。
此外,由于它们用作数组的索引,因此只有正值才有效。
将我们限制为正值,转换为 unsigned int
根本不会改变值。它仅允许我们使用 MSB,以防发生 int
溢出。对于无符号整数,最高位并不意味着负值,当我们将其移位 1 时,符号和值与我们预期的一样。
如果没有这两个约束,您的代码将无法运行。一旦 low
和 high
已经是无符号整数,您可能会再次遇到相同的溢出问题。 (我知道,C 标准不会将其称为无符号值的溢出,但这并不能解决问题。)在这种情况下,溢出位不会出现在变量中,并且在移位后会得到错误的值。
无论如何,第二个版本确实解决了这个问题:
如果您减去 2 个都在 0..INT_MAX
范围内的数字,您也会得到该范围内的结果。 (鉴于从较大的减去较小的。)
从基础数学我们知道 low
+mid
也必须在这个范围内,因为 mid
小于 high
。
所以我们不会在这里遇到溢出问题。
答案 2 :(得分:2)
两者都通过确保不超出允许范围来工作,并利用两个值都不能为负的事实。
后者很简单:它使用减法来确保值永远不会超过 high
的值。
前者使用不同的技术:它通过增加允许范围来绕过问题。如果将两个 N 位数字相加,则结果最多为 N+1 位。我们使用没有符号位的无符号数获得了额外的位。
请注意,位移 (>> 1
) 不提供任何额外值;您也可以使用除法 (/ 2
),因为数字是无符号的。
答案 3 :(得分:2)
它们(仅)有效,因为已知 low
和 high
至少为 0。
((unsigned int)low + (unsigned int)high)) >> 1
正有符号整数总是小于相应类型的最大无符号整数的一半,因为 unsigned 获得了一个额外的范围位。如果 low
或 high
可能为负数,则无符号加法中可能存在“溢出”1。
low + (high - low) / 2
在 high - low
中永远不会出现下溢,因为只有当 high
为负时才会出现下溢。永远不会有溢出,因为只有在 low
为负时才会发生。 result / 2
永远不会溢出或下溢,您总是会得到一个接近于 0 的值。将它加回到 low
永远不会溢出,因为结果永远不会超过 high
答案 4 :(得分:1)
什么时候可能溢出?如果 low
和 high
都接近 INT_MAX
。
为简单起见考虑字节大小。
如果我们将 0x7D
和 0x7F
(125
和 127
)相加,我们得到 0xFC = 0b11111100
。
如果我们将此值解释为有符号,则有 -4
,如果我们将此值解释为无符号,则有 252
。
有符号右移作为算术移位SAR
,用符号位填充左边部分,给出0b11111110
,有符号-2
(注意与整数除以2的结果相同)。我们绝对不希望 125
和 127
的平均值出现这样的结果。
无符号右移作为移位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_t
或 unsigned long
。或者只是long
:那么我们可以在 20 年后再次谈论它。
1988 年的 K&R C 书在他们自己的 qsort 中采用 int 时也有同样的错误。库之一在原型中有 size_t 。仅当有人试图对比 SIZE_MAX 大一半的字符数组进行排序时,这将因草率的平均公式而失败 - 这很容易就是 ULONG_MAX。