【问题标题】:Why prefer start + (end - start) / 2 over (start + end) / 2 when calculating the middle of an array?为什么在计算数组的中间时更喜欢 start + (end - start) / 2 而不是 (start + end) / 2?
【发布时间】:2016-12-05 21:29:11
【问题描述】:

我见过程序员使用公式

mid = start + (end - start) / 2

而不是使用更简单的公式

mid = (start + end) / 2

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

他们为什么使用前者?

【问题讨论】:

  • 大胆猜测:(start + end) 可能会溢出,而(end - start) 不会。
  • 因为当startend是指针时后者不起作用。
  • start + (end - start) / 2 也带有语义含义:(end - start) 是长度,所以这里说:start + half the length
  • @LưuVĩnhPhúc:这个问题不是有最好的答案和最多的选票吗?如果是这样,其他问题可能应该作为这个问题的副本关闭。帖子的年龄无关紧要。

标签: c algorithm


【解决方案1】:

有三个原因。

首先,即使您使用指针,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

其次,如果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!

脚注

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

【讨论】:

  • (end - start)signed int 情况下的结果在溢出时未定义。
  • 你能证明end-start不会溢出吗? AFAIK,如果您接受否定的start,则应该有可能使其溢出。当然,大多数时候当你计算平均值时,你知道值是>= 0 ...
  • @Bakuriu:不可能证明不正确的事情。
  • 它对 C 语言特别感兴趣,因为指针减法(根据标准)被设计破坏了。允许实现创建大到 end - start 未定义的数组,因为对象大小是无符号的,而指针差异是有符号的。所以end - start“即使使用指针也能工作”,前提是您还以某种方式将数组的大小保持在PTRDIFF_MAX 以下。公平地说,这对大多数架构来说并不是什么大问题,因为它是内存映射大小的一半。
  • @Bakuriu:顺便说一句,如果您认为我遗漏了某些内容或不清楚的内容,您可以使用帖子上的“编辑”按钮来建议更改(或自己进行更改) .我只是一个人,这个帖子已经被两千多双眼球看到了。那种评论,“你应该澄清......”真的让我很反感。
【解决方案2】:

我们可以举一个简单的例子来证明这个事实。假设在某个 large 数组中,我们试图找到[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) 不会溢出

【讨论】:

  • 如果INT_MAX加1,结果不会是负数,而是未定义。
  • @celtschk 从理论上讲,是的。实际上,它会从INT_MAX-INT_MAX 循环很多次。不过,依赖它是一个坏习惯。
【解决方案3】:

为了补充其他人已经说过的内容,第一个对那些缺乏数学头脑的人更清楚地解释了它的含义:

mid = start + (end - start) / 2

读作:

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

而:

mid = (start + end) / 2

读作:

mid 等于 start 加 end 的一半

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

正如科斯指出的那样,它也可以阅读:

mid 等于 start 和 end 的平均值

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

【讨论】:

  • 我明白你的意思,但这确实有点牵强。如果您看到“e - s”并想到“长度”,那么您几乎肯定会看到“(s+e)/2”并想到“平均”或“中”。
  • @djechlin 程序员的数学很差。他们忙于工作。他们没有时间参加数学课。
【解决方案4】:

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

【讨论】:

    猜你喜欢
    • 2021-07-04
    • 2019-09-07
    • 2017-10-29
    • 1970-01-01
    • 1970-01-01
    • 2021-12-19
    • 1970-01-01
    • 1970-01-01
    • 2011-08-04
    相关资源
    最近更新 更多