Почему предпочтение start + (end - start) / 2 более (start + end) / 2 при вычислении середины массива?

160

Я видел, как программисты используют формулу

mid = start + (end - start) / 2

вместо использования более простой формулы

mid = (start + end) / 2

для нахождения среднего элемента в массиве или списке.

Почему они используют первый?

Паллави Чаухан
источник
51
Дикая догадка: (start + end)может переполниться, а (end - start)не может.
Каданилук
30
потому что последний не работает, когда startи endуказатель.
Ensc
20
start + (end - start) / 2также несет смысловое значение: (end - start)длина, так это говорит: start + half the length.
njzk2
2
@ LưuVĩnhPhúc: разве у этого вопроса нет лучших ответов и наибольшее количество голосов? Если так, то другие вопросы, вероятно, следует закрыть как дубликат этого. Возраст постов не имеет значения.
Nisse Engström

Ответы:

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не переполнится, если startи endбудут большие положительные числа. Со знаковыми операндами переполнение не определено:

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

(Обратите внимание, что end - startможет переполниться, но только если start < 0или end < 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
Можете ли вы доказать, что не end-startпереполнится? AFAIK, если вы берете негатив, startдолжно быть возможно, чтобы он переполнился. Конечно, в большинстве случаев, когда вы вычисляете среднее значение, вы знаете, что значения >= 0...
Бакуриу
12
@Bakuriu: невозможно доказать что-то, что не соответствует действительности.
Дитрих Эпп
4
Это представляет особый интерес в C, так как вычитание указателя (согласно стандарту) нарушено по конструкции. Реализациям разрешено создавать массивы настолько большие, что они end - startне определены, поскольку размеры объектов не имеют знака, а различия указателей подписываются. Так что end - start«работает даже с использованием указателей», при условии, что вы также каким-то образом сохраняете размер массива ниже PTRDIFF_MAX. Чтобы быть справедливым по отношению к стандарту, это не является большим препятствием на большинстве архитектур, так как это вдвое меньше карты памяти.
Стив Джессоп
3
@Bakuriu: Кстати, в сообщении есть кнопка «Изменить», которую вы можете использовать, чтобы предложить изменения (или внести их самостоятельно), если вы думаете, что я что-то пропустил, или что-то неясно. Я всего лишь человек, и этот пост видели более двух тысяч пар глазных яблок. Вид комментария «Вы должны уточнить ...» действительно меня теряет.
Дитрих Эпп
18

Мы можем взять простой пример, чтобы продемонстрировать этот факт. Предположим, в некотором большом массиве мы пытаемся найти середину диапазона [1000, INT_MAX]. Теперь INT_MAXэто наибольшее значение, которое intможет хранить тип данных. Даже если 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) который не будет переполнен .

Shubham
источник
1
Если вы добавите 1 к INT_MAX, результат будет не отрицательным, а неопределенным.
celtschk
@celtschk Теоретически да. Практически это будет обернуть вокруг много времени , идущего от INT_MAXк -INT_MAX. Это плохая привычка полагаться на это, хотя.
Мачт
17

Чтобы добавить к тому, что уже сказали другие, первый объясняет его значение более понятным для менее математически мыслящих:

mid = start + (end - start) / 2

читается как:

середина равна началу плюс половина длины.

в то время как:

mid = (start + end) / 2

читается как:

середина равна половине начала плюс конец

Что не так ясно, как первое, по крайней мере, когда выражено так.

как указал Кос, он также может читать:

середина равна среднему значению начала и конца

Что яснее, но все же не так, по крайней мере, на мой взгляд, так же ясно, как первое.

TheLethalCoder
источник
3
Я понимаю вашу точку зрения, но это действительно натянуто. Если вы видите «e - s» и думаете «длина», то вы почти наверняка видите «(s + e) ​​/ 2» и думаете «среднее» или «среднее».
Джечлин
2
@djechlin Программисты плохо разбираются в математике. Они заняты выполнением своей работы. У них нет времени посещать математические занятия.
Маленький инопланетянин
1

start + (end-start) / 2 позволяет избежать возможного переполнения, например start = 2 ^ 20 и end = 2 ^ 30

Бойцовский клуб
источник