Чому при обчисленні середини масиву віддають перевагу 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.