Чому при обчисленні середини масиву віддають перевагу start + (end - start) / 2 over (start + end) / 2?


160

Я бачив, як програмісти використовують формулу

mid = start + (end - start) / 2

замість того, щоб використовувати більш просту формулу

mid = (start + end) / 2

для пошуку середнього елемента в масиві чи списку.

Для чого вони використовують колишню?


51
Дива здогадка: (start + end)може переповнюватись, а поки (end - start)не може.
каданілюк

30
тому що останні не працюють, коли startі endвказують.
січня


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


Результат (end - start)у signed intвипадку не визначений, коли він переповнюється.
січня

Чи можете ви довести, що end-startзвичайне переповнення? AFAIK, якщо ви ставитесь до негативу, startйого слід переповнювати. Звичайно, у більшості випадків, коли ви обчислюєте середнє значення, ви знаєте, що значення >= 0...
Bakuriu

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" і думаєте "середній" або "середина".
djechlin

2
@djechlin Програмісти погані в математиці. Вони зайняті своєю роботою. У них немає часу відвідувати уроки математики.
Маленький прибулець

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.