在计算数组的中间部分时,为什么选择 start + (end-start)/2而不是 start + end/2?

我见过程序员用这个公式

mid = start + (end - start) / 2

而不是使用更简单的公式

mid = (start + end) / 2

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

他们为什么用前者?

16062 次浏览

有三个原因。

首先,只要 end - start不溢出 1,即使使用指针,start + (end - start) / 2也能正常工作。

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

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

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!

脚注

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

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

还有 start = 1000end = 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).

为了补充其他人已经说过的话,第一条对那些不太懂数学的人解释得更清楚:

mid = start + (end - start) / 2

内容如下:

Mid 等于 start 加上长度的一半。

而:

mid = (start + end) / 2

内容如下:

中等于开始加结束的一半

这似乎不像第一个那么清楚,至少在这样表达的时候。

正如科斯指出的那样:

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

这一点很清楚,但至少在我看来,还不如第一点清楚。

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