Як я можу забезпечити, щоб ділення цілих чисел завжди було округлене?


242

Я хочу забезпечити, щоб ділення цілих чисел завжди було округлене, якщо це необхідно. Чи є кращий спосіб, ніж цей? Кастинг триває багато. :-)

(int)Math.Ceiling((double)myInt1 / myInt2)

48
Чи можете ви чіткіше визначити те, що вважаєте «кращим»? Швидше? Коротше? Точніше? Більш міцний? Більш очевидно правильно?
Ерік Ліпперт

6
У вас завжди багато кастингу з математикою на C # - тому це не чудова мова для подібних речей. Ви хочете, щоб значення округлили вгору або від нуля - слід -3,1 перейти до -3 (вгору) або -4 (від нуля)
Кіт

9
Ерік: Що ви маєте на увазі під "точнішим? Більш надійним? Більш очевидно правильним?" Насправді те, що я мав на увазі, було просто "краще", я б дозволив читачеві вкласти сенс у краще. Тож якщо у когось був коротший фрагмент коду, чудово, якщо інший мав швидший, також чудовий :-) А як щодо вас є якісь пропозиції?
Карстен

1
Я єдиний, хто, прочитавши заголовок, "О, це якась сукупність C #?"
Метт Бал

6
Дійсно дивно, наскільки тонко складним виявилося це питання та наскільки повчальною була дискусія.
Джастін Морган

Відповіді:


669

ОНОВЛЕННЯ: Це питання було предметом мого блогу в січні 2013 року . Дякую за чудове запитання!


Отримати правильну арифметику з цілим числом важко. Як це було показано на сьогоднішній день, у той момент, коли ви намагаєтесь зробити «розумний» трюк, шанси на те, що ви помилилися. А коли виявлено недолік, змінивши код, щоб виправити недолік, не враховуючи, чи виправлення порушує щось інше помилку , не є хорошою технікою вирішення проблем. Поки у нас було, я думаю, п'ять різних невірних цілочисельних арифметичних рішень цієї абсолютно не особливо складної проблеми.

Правильний спосіб підходу до цілочисленних арифметичних задач - тобто спосіб, який збільшує ймовірність отримання відповіді правильним в перший раз - це підходити до проблеми обережно, вирішувати її крок за часом і використовувати хороші інженерні принципи при цьому так.

Почніть з читання специфікації того, що ви намагаєтеся замінити. У специфікації для цілого поділу чітко зазначено:

  1. Ділення округляє результат до нуля

  2. Результат дорівнює нулю або позитиву, коли обидва операнди мають однаковий знак і нуль або мінус, коли два операнди мають протилежні знаки

  3. Якщо лівий операнд є найменшим представницьким int, а правий операнд –1, відбувається переповнення. [...] це визначається реалізацією щодо того, чи [ArithmeticException] кинуто чи переповнення не віднесено, а отримане значення - значення лівого операнда.

  4. Якщо значення правого операнда дорівнює нулю, викидається System.DivideByZeroException.

Ми хочемо - це ціла функція ділення, яка обчислює коефіцієнт, але округлює результат завжди вгору , а не завжди до нуля .

Тому напишіть специфікацію для цієї функції. Наша функція int DivRoundUp(int dividend, int divisor)повинна визначати поведінку для кожного можливого введення. Ця невизначена поведінка викликає занепокоєння, тому давайте її усунемо. Ми скажемо, що наша операція має цю специфікацію:

  1. операція кидає, якщо дільник дорівнює нулю

  2. операція кидає, якщо дивіденд int.minval, а дільник -1

  3. якщо немає залишку - поділ є "рівним" - тоді повернене значення є інтегральним коефіцієнтом

  4. В іншому випадку він повертає найменше ціле число, що перевищує коефіцієнт, тобто завжди округляється.

Зараз у нас є специфікація, тому ми знаємо, що можемо придумати тестовий дизайн . Припустимо, ми додамо додатковий критерій проектування, щоб задачу вирішували виключно з цілої арифметикою, а не обчислювали коефіцієнт як подвійний, оскільки рішення "подвійний" було явно відхилено в заяві проблеми.

То що ми повинні обчислити? Зрозуміло, що для задоволення нашої специфікації, залишаючись виключно в цілій арифметиці, нам потрібно знати три факти. По-перше, яким був цілий коефіцієнт? По-друге, чи був дивізіон без залишку? І по-третє, якщо ні, чи був обчислений цілий коефіцієнт шляхом округлення вгору чи вниз?

Тепер, коли у нас є специфікація та дизайн, ми можемо почати писати код.

public static int DivRoundUp(int dividend, int divisor)
{
  if (divisor == 0 ) throw ...
  if (divisor == -1 && dividend == Int32.MinValue) throw ...
  int roundedTowardsZeroQuotient = dividend / divisor;
  bool dividedEvenly = (dividend % divisor) == 0;
  if (dividedEvenly) 
    return roundedTowardsZeroQuotient;

  // At this point we know that divisor was not zero 
  // (because we would have thrown) and we know that 
  // dividend was not zero (because there would have been no remainder)
  // Therefore both are non-zero.  Either they are of the same sign, 
  // or opposite signs. If they're of opposite sign then we rounded 
  // UP towards zero so we're done. If they're of the same sign then 
  // we rounded DOWN towards zero, so we need to add one.

  bool wasRoundedDown = ((divisor > 0) == (dividend > 0));
  if (wasRoundedDown) 
    return roundedTowardsZeroQuotient + 1;
  else
    return roundedTowardsZeroQuotient;
}

Це розумний? Ні. Красиві? Ні Короткий? Ні. Правильно відповідно до специфікації? В це я вірю, але не до кінця перевірив це.Це виглядає досить добре, хоча.

Ми тут професіонали; використовувати хороші інженерні практики. Дослідіть свої інструменти, вкажіть бажану поведінку, спочатку розгляньте випадки помилок та напишіть код, щоб підкреслити його очевидну правильність. І коли ви знайдете помилку, подумайте, чи має ваш алгоритм глибокий хиб для початку, перш ніж ви просто випадковим чином почнете міняти напрями порівнянь і розбивати речі, які вже працюють.


44
Відмінна зразкова відповідь
Гавін Міллер

61
Мене хвилює не поведінка; будь-яка поведінка здається виправданою. Мене хвилює те, що він не вказаний , а значить, його неможливо легко перевірити. У цьому випадку ми визначаємо власного оператора, тому ми можемо вказати, яку поведінку нам подобається. Мені байдуже, чи така поведінка є "кидок" чи "не кидайте", але мені все одно, що це буде заявлено.
Ерік Ліпперт

68
Чорт забирай, педантизм провалюється :(
Джон Скіт

32
Людина - Чи не могли б ви написати про це книгу?
xtofl

76
@finnw: Я тестував це чи ні, не має значення. Вирішення цієї цілочисельної арифметичної задачі - це не моя бізнес проблема; якби воно було, то я б його тестував. Якщо хто - то хоче взяти код від сторонніх через інтернет , щоб вирішити свої бізнес - проблеми , то відповідальність лежить на них , щоб ретельно перевірити його.
Ерік Ліпперт

49

Усі відповіді тут поки здаються надто складними.

У C # та Java для отримання позитивного дивіденду та дільника вам просто потрібно зробити:

( dividend + divisor - 1 ) / divisor 

Джерело: Перетворення чисел, Roland Backhouse, 2001


Дивовижно. Хоча, вам слід було додати дужки, щоб усунути неясності. Доказ цього був трохи довгим, але ви можете відчути це в кишечнику, що це правильно, просто подивившись на нього.
Йорген Зіґвардссон

1
Гммм ... що з дивідендом = 4, дільник = (- 2) ??? 4 / (-2) = (-2) = (-2) після округлення. але алгоритм, який ви надали (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0,5) = 0 після округлення.
Скотт

1
@Scott - вибачте, я пропустив зазначити, що це рішення стосується лише позитивного дивіденду та дільника. Я оновив свою відповідь, щоб згадати про уточнення цього.
Ян Нельсон

1
мені це подобається, звичайно, ти можеш мати дещо штучне переповнення чисельника як побічний продукт цього підходу ...
TCC

2
@PIntag: Ідея хороша, але використання модуля неправильне. Візьміть 13 і 3. Очікуваний результат 5, але ((13-1)%3)+1)дає 1 як результат. Прийняття правильного виду поділу 1+(dividend - 1)/divisorдає такий же результат, як і відповідь на позитивний дивіденд та дільник. Також немає проблем із переповненням, якими б штучними вони не були.
Лутц Леман

48

Остаточна відповідь на основі int

Для підписаних цілих чисел:

int div = a / b;
if (((a ^ b) >= 0) && (a % b != 0))
    div++;

Для цілих непідписаних чисел:

int div = a / b;
if (a % b != 0)
    div++;

Міркування цієї відповіді

Ціле ділення "/ ' визначається на округлення до нуля (7.7.2 специфікації), але ми хочемо округлити його. Це означає, що негативні відповіді вже округлені правильно, але позитивні відповіді потрібно скоригувати.

Ненульові позитивні відповіді легко виявити, але нульові відповіді трохи складніше, оскільки це може бути або округленням негативного значення, або округленням позитивного.

Найбезпечніша ставка - виявити, коли відповідь має бути позитивною, перевіривши, чи знаки обох цілих чисел однакові. Оператор ^цілого xor ' ' на двох значеннях призведе до знаку 0-біт, коли це так, це означає негативний результат, тому перевірка(a ^ b) >= 0 визначає, що результат повинен був бути позитивним перед округленням. Також зауважте, що для непідписаних цілих чисел кожна відповідь очевидно позитивна, тому цю перевірку можна опустити.

Залишилася лише перевірка, чи відбулося якесь округлення, для якого a % b != 0буде виконана робота.

Навчені уроки

Арифметика (ціле чи інше) не так просто, як здається. Думати ретельно потрібно в усі часи.

Крім того, хоча моя остаточна відповідь, мабуть, не така «проста» чи «очевидна» або, можливо, навіть «швидка», як відповідає плаваюча точка, для мене є одна дуже сильна викупна якість; Я зараз міркував через відповідь, тому я фактично впевнений, що це правильно (поки хтось розумніший не скаже мені інакше - шалений погляд в бік Еріка -).

Щоб отримати таке ж впевненість у відповіді на плаваючу точку, мені доведеться більше (і, можливо, складніше) думати про те, чи існують умови, за яких точність з плаваючою комою може перешкоджати, і чи Math.Ceiling можливо щось небажане на "правильних" входах.

Шлях пройшов

Замінити (зверніть увагу , я замінив другий myInt1з myInt2, припускаючи , що було то , що ви мали в виду):

(int)Math.Ceiling((double)myInt1 / myInt2)

з:

(myInt1 - 1 + myInt2) / myInt2

Єдине застереження полягає в тому, що якщо myInt1 - 1 + myInt2переповнюється цілий тип, який ви використовуєте, ви не можете отримати те, що очікуєте.

Причина цього неправильна : -1000000 і 3999 мають дати -250, це -249

EDIT:
Зважаючи на те, що ця помилка має таку саму помилку, як і для іншого цілого рішення для негативних myInt1значень, може бути простіше зробити щось на кшталт:

int rem;
int div = Math.DivRem(myInt1, myInt2, out rem);
if (rem > 0)
  div++;

Це має дати правильний результат у divвикористанні лише цілих операцій.

Причина це неправильно : -1 і -5 мають дати 1, це дає 0

EDIT (ще раз, з почуттям):
Оператор поділу кружляє до нуля; для негативних результатів це абсолютно правильно, тому коригувати потрібно лише негативні результати. Також враховуючи, що це DivRemпросто так /і %все одно, давайте пропустимо виклик (і почнемо з простого порівняння, щоб уникнути обчислення модуля, коли він не потрібен):

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

Причина це неправильно : -1 і 5 повинні дати 0, це дає 1

(На мою власну захист останньої спроби я ніколи не повинен був намагатися обґрунтувати відповідь, поки мій розум сказав мені, що я запізнююсь на дві години)


19

Ідеальний шанс використовувати метод розширення:

public static class Int32Methods
{
    public static int DivideByAndRoundUp(this int number, int divideBy)
    {                        
        return (int)Math.Ceiling((float)number / (float)divideBy);
    }
}

Це робить ваш код також зручним для читання:

int result = myInt.DivideByAndRoundUp(4);

1
Ем, що? Ваш код буде називатися myInt.DivideByAndRoundUp () і завжди повертатиме 1, крім введення 0, який спричинив би виняток ...
конфігуратор

5
Епічний збій. (-2) .DivideByAndRoundUp (2) повертається 0.
Timwi

3
Я дуже спізнююся на вечірку, але чи складається цей код? Мій клас Math не містить методу Ceiling, який бере два аргументи.
Р. Мартіньо Фернандес

17

Можна написати помічника.

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}

1
Ще однакова кількість кастингу триває, хоча
ChrisF

29
@Outlaw, припустимо все, що ти хочеш. Але для мене, якщо вони не ставлять цього питання, я, як правило, припускаю, що вони цього не розглядали.
JaredPar

1
Писати помічника марно, якщо це просто так. Натомість напишіть довідник із вичерпним набором тестів.
долмен

3
@dolmen Ви знайомі з концепцією повторного використання коду ? оо
Rushyo

4

Ви можете скористатися чимось подібним.

a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)

12
Цей код, очевидно, помиляється двома способами. По-перше, є незначна помилка в синтаксисі; вам потрібно більше дужок. Але ще важливіше, що він не обчислює бажаний результат. Наприклад, спробуйте тестувати з = -1000000 і b = 3999. Результат регулярного цілого поділу дорівнює -250. Подвійний поділ - -250.0625 ... Бажана поведінка - округнути. Очевидно, що правильне округлення від -250.0625 означає округлення до -250, але ваш код до -249.
Ерік Ліпперт

36
Вибачте, що вам потрібно це продовжувати говорити, але ваш код - ПОСЛІДНО ВІДКРИТИ Даніель. 1/2 має округлювати UP до 1, але ваш код округляє його ВНИЗ до 0. Кожен раз, коли я знаходжу помилку, ви "виправляєте" її, вводячи ще одну помилку. Моя порада: припиніть це робити. Коли хтось знайде помилку у вашому коді, не просто плескайте разом виправлення, не продумуючи, що саме спричинило помилку. Використовувати добрі інженерні практики; знайти недолік в алгоритмі та виправити його. Недолік у всіх трьох неправильних версіях вашого алгоритму полягає в тому, що ви неправильно визначаєте, коли округлення було "вниз".
Ерік Ліпперт

8
Неймовірно, скільки помилок може бути в цьому невеликому фрагменті коду. Я ніколи не мав багато часу подумати над цим - результат проявляється в коментарях. (1) a * b> 0 було б правильним, якби він не перелив. Для знаків a і b існує 9 комбінацій - [-1, 0, +1] x [-1, 0, +1]. Ми можемо ігнорувати випадок b == 0, залишаючи 6 випадків [-1, 0, +1] x [-1, +1]. a / b округлюється до нуля, тобто округлюється до негативних результатів і округлюється до позитивних результатів. Отже, коригування потрібно здійснити, якщо a і b мають однаковий знак і не є обома нульовими.
Даніель Брюкнер

5
Ця відповідь - це, мабуть, найгірше, що я написав на ЗО ... і це зараз пов’язане блогом Еріка ... Ну, мій намір полягав не в тому, щоб дати зрозуміле рішення; Я дійсно замикався на короткий і швидкий злом. І щоб захистити своє рішення ще раз, я вперше зрозумів ідею, але не думав про переливи. Очевидно, моя помилка була розмістити код без написання та тестування його у VisualStudio. "Виправлення" ще гірші - я не зрозумів, що це проблема переповнення, і подумав, що допустив логічну помилку. Як наслідок, перші "виправлення" нічого не змінили; Я щойно перевернув
Даніель Брюкнер

10
логіки і штовхнув помилку. Тут я допустив наступні помилки; як Ерік уже згадував, я не дуже аналізував помилку і просто зробив перше, що здавалося правильним. І я досі не користувався VisualStudio. Гаразд, я поспішав і не витратив більше п'яти хвилин на "виправлення", але це не повинно бути виправданням. Після того, як я Ерік кілька разів вказував на помилку, я запустив VisualStudio і виявив справжню проблему. Виправлення за допомогою Sign () робить річ ще більш нечитабельною і перетворює її в код, який ви насправді не хочете підтримувати. Я засвоїв свій урок і більше не буду недооцінювати, наскільки хитрий
Даніель Брюкнер

-2

Деякі з вищезазначених відповідей використовують поплавці, це неефективно і справді не потрібно. Для неподписаних ints це ефективна відповідь для int1 / int2:

(int1 == 0) ? 0 : (int1 - 1) / int2 + 1;

Для підписаних даних це буде неправильно


Не те, що ОП запитували в першу чергу, насправді не додає до інших відповідей.
santamanno

-4

Проблема з усіма рішеннями тут полягає або в тому, що їм потрібен склад, або вони мають числову проблему. Кастинг в плаву чи подвійне - це завжди варіант, але ми можемо зробити краще.

Коли ви використовуєте код відповіді від @jerryjvl

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

є помилка округлення. 1/5 буде закруглено, тому що 1% 5! = 0. Але це неправильно, оскільки округлення відбудеться лише в тому випадку, якщо ви заміните 1 на 3, тому результат дорівнює 0,6. Нам потрібно знайти спосіб округлення, коли обчислення дають нам значення, що перевищує 0,5. Результат модуля оператора у верхньому прикладі має діапазон від 0 до myInt2-1. Округлення відбудеться лише в тому випадку, якщо залишок більше 50% від дільника. Отже, скоригований код виглядає приблизно так:

int div = myInt1 / myInt2;
if (myInt1 % myInt2 >= myInt2 / 2)
    div++;

Звичайно, у myInt2 / 2 у нас теж є проблема округлення, але цей результат дасть вам краще рішення для округлення, ніж інші на цьому сайті.


"Нам потрібно знайти спосіб округлення, коли обчислення дають нам значення, що перевищує або дорівнює 0,5" - ви пропустили точку цього питання - або завжди округлюєте, тобто ОП хоче округлити 0,001 до 1.
Grhm
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.