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

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

mid = start + (end - start) / 2

вместо простой формулы

mid = (start + end) / 2

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

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

Ответ 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

Во-вторых, start + (end - start) / 2 не будет переполняться, если start и end - большие положительные числа. С подписанными операндами переполнение undefined:

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, если результат вычитания указателя не представляется в виде ptrdiff_t, то поведение undefined. Однако на практике это требует выделения массива char, использующего по меньшей мере половину всего адресного пространства.

Ответ 2

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

Ответ 3

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

mid = start + (end - start) / 2

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

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

тогда:

mid = (start + end) / 2

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

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

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

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

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

Что яснее, но по-прежнему нет, по крайней мере, по-моему, так же ясно, как и первое.

Ответ 4

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