Почему предпочтение 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) который не будет переполнен .


1
Если вы добавите 1 к INT_MAX, результат будет не отрицательным, а неопределенным.
— celtschk

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

17

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

mid = start + (end - start) / 2

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

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

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

mid = (start + end) / 2

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

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

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

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

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

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


3
Я понимаю вашу точку зрения, но это действительно натянуто. Если вы видите «e - s» и думаете «длина», то вы почти наверняка видите «(s + e) ​​/ 2» и думаете «среднее» или «среднее».
— Джечлин

2
@djechlin Программисты плохо разбираются в математике. Они заняты выполнением своей работы. У них нет времени посещать математические занятия.
— Маленький инопланетянин

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.