在计算数组的中间位置时,为什么比(start + end)/ 2更喜欢start +(end-start)/ 2?


160

我看过程序员使用公式

mid = start + (end - start) / 2

而不是使用更简单的公式

mid = (start + end) / 2

用于查找数组或列表中的中间元素。

他们为什么使用前者?


51
疯狂猜测:(start + end)可能会溢出,而(end - start)不会。
cadaniluk '16

30
因为后者在startend作为指针时不起作用。
ensc


20
start + (end - start) / 2也带有语义含义:(end - start)是长度,因此表示:start + half the length
njzk2 '16

2
@LưuVĩnhPhúc:这个问题不是答案最好,投票最多吗?如果是这样的话,其他问题可能应该作为该问题的一个重复来解决。职位的年龄无关紧要。
NisseEngström'8

Answers:


218

有三个原因。

首先,start + (end - start) / 2即使您使用的是指针,只要end - start不溢出1,它都可以工作

int *start = ..., *end = ...;
int *mid = start + (end - start) / 2; // works as expected
int *mid = (start + end) / 2;         // type error, won't compile

其次,start + (end - start) / 2如果startend为大正数,则不会溢出。对于带符号的操作数,未定义溢出:

int start = 0x7ffffffe, end = 0x7fffffff;
int mid = start + (end - start) / 2; // works as expected
int mid = (start + end) / 2;         // overflow... undefined

(请注意,这end - start可能会溢出,但前提是start < 0end < 0。)

或使用无符号算术定义了溢出,但给出了错误的答案。但是,对于无符号操作数,start + (end - start) / 2只要存在,就永远不会溢出end >= start

unsigned start = 0xfffffffeu, end = 0xffffffffu;
unsigned mid = start + (end - start) / 2; // works as expected
unsigned mid = (start + end) / 2;         // mid = 0x7ffffffe

最后,您通常想四舍五入start

int start = -3, end = 0;
int mid = start + (end - start) / 2; // -2, closer to start
int mid = (start + end) / 2;         // -1, surprise!

脚注

1根据C标准,如果指针减法的结果不能表示为a ptrdiff_t,则该行为不确定。但是,实际上,这需要char至少使用整个地址空间的一半来分配数组。


(end - start)signed int情况的结果在溢出时是不确定的。
ensc '16

你能证明end-start不会溢出吗?AFAIK如果采取负值start,应该可以使其溢出。当然,在大多数情况下,计算平均值时,您会知道值是>= 0...
Bakuriu

12
@Bakuriu:不可能证明不正确的东西。
Dietrich Epp

4
由于C的指针减法(按标准)被设计破坏了,因此在C中特别有趣。允许实现创建大到end - start无法定义的数组,因为对象大小是无符号的,而指针差异是有符号的。因此,end - start“即使使用指针也可以工作”,只要您还以某种方式将数组的大小保持在下面PTRDIFF_MAX。公平地说,对于大多数架构而言,这并不是什么障碍,因为它只有内存映射的一半。
Steve Jessop

3
@Bakuriu:顺便说一句,帖子上有一个“编辑”按钮,如果您认为我错过了某些内容或不清楚的地方,可以用来建议更改(或自己进行更改)。我只是人类,这个帖子已经被两千多对眼球所吸引。这种评论“您应该澄清...”确实使我误解了。
Dietrich Epp

18

我们可以举一个简单的例子来证明这一事实。假设在某个较大的数组中,我们试图找到range的中点[1000, INT_MAX]。现在,INT_MAXint数据类型可以存储的最大值。即使1加上此值,最终值也将变为负数。

另外,start = 1000还有end = INT_MAX

使用公式:(start + end)/2

中点将是

(1000 + INT_MAX)/2= -(INT_MAX+999)/2,这是负数,如果尝试使用此值建立索引,则可能会产生细分错误

但是,使用公式(start + (end-start)/2),我们得到:

(1000 + (INT_MAX-1000)/2)= (1000 + INT_MAX/2 - 500)= (INT_MAX/2 + 500) 不会溢出


1
如果将1加到INT_MAX,结果将不是负数,而是不确定的。
celtschk '16

@celtschk从理论上讲,是的。实际上,从INT_MAX到,它将绕过很多时间-INT_MAX。依靠它是一个坏习惯。
2016年

17

除了其他人已经说过的内容外,第一个解释对于那些数学上不太懂的人也更清楚地解释了它的含义:

mid = start + (end - start) / 2

读为:

中间等于开始加上长度的一半。

而:

mid = (start + end) / 2

读为:

中等于开始加结束的一半

至少当这样表达时,它似乎并不像第一个那样清晰。

正如Kos指出的那样,它也可以读为:

中值等于开始和结束的平均值

至少在我看来,哪一个更清晰,但仍不如第一个清晰。


3
我明白你的意思,但这确实是一个难题。如果看到“ e-s”并认为“ length”,那么几乎可以肯定地看到“(s + e)/ 2”并认为“平均值”或“中间”。
djechlin'8

2
@djechlin程序员的数学能力很差。他们正忙于工作。他们没有时间去上数学课。
Little Alien'8

1

start +(end-start)/ 2可以避免可能的溢出,例如start = 2 ^ 20和end = 2 ^ 30

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.