Я хочу забезпечити, щоб ділення цілих чисел завжди було округлене, якщо це необхідно. Чи є кращий спосіб, ніж цей? Кастинг триває багато. :-)
(int)Math.Ceiling((double)myInt1 / myInt2)
Я хочу забезпечити, щоб ділення цілих чисел завжди було округлене, якщо це необхідно. Чи є кращий спосіб, ніж цей? Кастинг триває багато. :-)
(int)Math.Ceiling((double)myInt1 / myInt2)
Відповіді:
ОНОВЛЕННЯ: Це питання було предметом мого блогу в січні 2013 року . Дякую за чудове запитання!
Отримати правильну арифметику з цілим числом важко. Як це було показано на сьогоднішній день, у той момент, коли ви намагаєтесь зробити «розумний» трюк, шанси на те, що ви помилилися. А коли виявлено недолік, змінивши код, щоб виправити недолік, не враховуючи, чи виправлення порушує щось інше помилку , не є хорошою технікою вирішення проблем. Поки у нас було, я думаю, п'ять різних невірних цілочисельних арифметичних рішень цієї абсолютно не особливо складної проблеми.
Правильний спосіб підходу до цілочисленних арифметичних задач - тобто спосіб, який збільшує ймовірність отримання відповіді правильним в перший раз - це підходити до проблеми обережно, вирішувати її крок за часом і використовувати хороші інженерні принципи при цьому так.
Почніть з читання специфікації того, що ви намагаєтеся замінити. У специфікації для цілого поділу чітко зазначено:
Ділення округляє результат до нуля
Результат дорівнює нулю або позитиву, коли обидва операнди мають однаковий знак і нуль або мінус, коли два операнди мають протилежні знаки
Якщо лівий операнд є найменшим представницьким int, а правий операнд –1, відбувається переповнення. [...] це визначається реалізацією щодо того, чи [ArithmeticException] кинуто чи переповнення не віднесено, а отримане значення - значення лівого операнда.
Якщо значення правого операнда дорівнює нулю, викидається System.DivideByZeroException.
Ми хочемо - це ціла функція ділення, яка обчислює коефіцієнт, але округлює результат завжди вгору , а не завжди до нуля .
Тому напишіть специфікацію для цієї функції. Наша функція int DivRoundUp(int dividend, int divisor)
повинна визначати поведінку для кожного можливого введення. Ця невизначена поведінка викликає занепокоєння, тому давайте її усунемо. Ми скажемо, що наша операція має цю специфікацію:
операція кидає, якщо дільник дорівнює нулю
операція кидає, якщо дивіденд int.minval, а дільник -1
якщо немає залишку - поділ є "рівним" - тоді повернене значення є інтегральним коефіцієнтом
В іншому випадку він повертає найменше ціле число, що перевищує коефіцієнт, тобто завжди округляється.
Зараз у нас є специфікація, тому ми знаємо, що можемо придумати тестовий дизайн . Припустимо, ми додамо додатковий критерій проектування, щоб задачу вирішували виключно з цілої арифметикою, а не обчислювали коефіцієнт як подвійний, оскільки рішення "подвійний" було явно відхилено в заяві проблеми.
То що ми повинні обчислити? Зрозуміло, що для задоволення нашої специфікації, залишаючись виключно в цілій арифметиці, нам потрібно знати три факти. По-перше, яким був цілий коефіцієнт? По-друге, чи був дивізіон без залишку? І по-третє, якщо ні, чи був обчислений цілий коефіцієнт шляхом округлення вгору чи вниз?
Тепер, коли у нас є специфікація та дизайн, ми можемо почати писати код.
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;
}
Це розумний? Ні. Красиві? Ні Короткий? Ні. Правильно відповідно до специфікації? В це я вірю, але не до кінця перевірив це.Це виглядає досить добре, хоча.
Ми тут професіонали; використовувати хороші інженерні практики. Дослідіть свої інструменти, вкажіть бажану поведінку, спочатку розгляньте випадки помилок та напишіть код, щоб підкреслити його очевидну правильність. І коли ви знайдете помилку, подумайте, чи має ваш алгоритм глибокий хиб для початку, перш ніж ви просто випадковим чином почнете міняти напрями порівнянь і розбивати речі, які вже працюють.
Усі відповіді тут поки здаються надто складними.
У C # та Java для отримання позитивного дивіденду та дільника вам просто потрібно зробити:
( dividend + divisor - 1 ) / divisor
((13-1)%3)+1)
дає 1 як результат. Прийняття правильного виду поділу 1+(dividend - 1)/divisor
дає такий же результат, як і відповідь на позитивний дивіденд та дільник. Також немає проблем із переповненням, якими б штучними вони не були.
Для підписаних цілих чисел:
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
(На мою власну захист останньої спроби я ніколи не повинен був намагатися обґрунтувати відповідь, поки мій розум сказав мені, що я запізнююсь на дві години)
Ідеальний шанс використовувати метод розширення:
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);
Можна написати помічника.
static int DivideRoundUp(int p1, int p2) {
return (int)Math.Ceiling((double)p1 / p2);
}
Ви можете скористатися чимось подібним.
a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)
Деякі з вищезазначених відповідей використовують поплавці, це неефективно і справді не потрібно. Для неподписаних ints це ефективна відповідь для int1 / int2:
(int1 == 0) ? 0 : (int1 - 1) / int2 + 1;
Для підписаних даних це буде неправильно
Проблема з усіма рішеннями тут полягає або в тому, що їм потрібен склад, або вони мають числову проблему. Кастинг в плаву чи подвійне - це завжди варіант, але ми можемо зробити краще.
Коли ви використовуєте код відповіді від @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 у нас теж є проблема округлення, але цей результат дасть вам краще рішення для округлення, ніж інші на цьому сайті.