Как я могу гарантировать, что деление целых чисел всегда округляется?


243

Я хочу убедиться, что деление целых чисел всегда округляется при необходимости. Есть ли лучший способ, чем этот? Происходит много кастингов. :-)

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

48
Можете ли вы более четко определить, что вы считаете «лучше»? Быстрее? Более короткие? Более точным? Более надежный? Более очевидно, правильно?
Эрик Липперт

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

9
Эрик: Что вы подразумеваете под «Более точным? Более надежным? Более правильным?» На самом деле то, что я имел в виду, было просто «лучше», я бы позволил читателю лучше понять смысл. Так что, если у кого-то был более короткий фрагмент кода, замечательно, если у другого был более быстрый, тоже великолепно :-) Как насчет ваших предложений?
Карстен

1
Я единственный, кто после прочтения заголовка сказал: «О, это какой-то обзор C #?»
Мэтт Болл

6
Удивительно, насколько тонко сложным оказался этот вопрос и насколько поучительным было обсуждение.
Джастин Морган

Ответы:


670

ОБНОВЛЕНИЕ: Этот вопрос был темой моего блога в январе 2013 года . Спасибо за отличный вопрос!


Получить правильную целочисленную арифметику сложно. Как было наглядно продемонстрировано, в тот момент, когда вы пытаетесь сделать «умный» трюк, вероятность того, что вы допустили ошибку, высока. И когда обнаружен недостаток, изменение кода для исправления недостатка без учета того, нарушает ли исправление что-либо еще , не является хорошим методом решения проблем. До сих пор у нас было пять различных неправильных целочисленных арифметических решений этой совершенно не особо сложной проблемы.

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

Начните с чтения спецификации того, что вы пытаетесь заменить. Спецификация для целочисленного деления четко гласит:

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

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

  3. Если левый операнд является наименьшим представимым типом int, а правый операнд равен –1, происходит переполнение. [...] это определяется реализацией относительно того, выбрасывается ли [ArithmeticException] или переполнение не сообщается, а полученное значение равно значению левого операнда.

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

Нам нужна целочисленная функция деления, которая вычисляет частное, но округляет результат всегда вверх , а не всегда к нулю .

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

  1. операция выбрасывает, если делитель равен нулю

  2. Операция выдает, если divind is int.minval и divisor равен -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;
}

Это умно? Нет. Красиво? Нет. Коротко? Правильно ли в соответствии со спецификацией? Я верю в это, но я не до конца проверил это. Это выглядит довольно хорошо, хотя.

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


45
Отличный примерный ответ
Гэвин Миллер

62
Меня волнует не поведение; любое поведение кажется оправданным. Что меня волнует, так это то, что он не указан , а значит, его нелегко проверить. В этом случае мы определяем наш собственный оператор, поэтому мы можем указать любое поведение, которое нам нравится. Меня не волнует, будет ли это поведение «бросать» или «не бросать», но мне важно, чтобы о нем говорилось.
Эрик Липперт

69
Черт возьми, педантизм потерпел неудачу :(
Джон Скит

33
Человек - Не могли бы вы написать книгу об этом, пожалуйста?
xtofl

77
@finnw: проверял я это или нет, не имеет значения. Решение этой целочисленной арифметической задачи не является моей деловой проблемой; если бы это было так, то я бы проверил это. Если кто-то хочет взять код у незнакомых людей из Интернета, чтобы решить свою бизнес-задачу, то это обязанность его тщательно проверить.
Эрик Липперт

49

Все ответы здесь пока кажутся слишком сложными.

В C # и Java для положительных дивидендов и делителей вам просто нужно сделать:

( dividend + divisor - 1 ) / divisor 

Источник: Преобразование чисел, Роланд Бэкхаус, 2001


Потрясающие. Хотя, вы должны были добавить круглые скобки, чтобы убрать неясности. Доказательство этого было немного длинным, но вы можете почувствовать, что это правильно, просто глядя на это.
Йорген Сигвардссон

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

1
@ Скотт - извините, я не упомянул, что это решение применимо только для положительных дивидендов и делителей. Я обновил свой ответ, чтобы упомянуть прояснить это.
Ян Нельсон

1
мне нравится, конечно, вы можете иметь несколько искусственное переполнение в числителе как побочный продукт этого подхода ...
TCC

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

48

Окончательный int-based ответ

Для целых чисел со знаком:

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

РЕДАКТИРОВАТЬ:
Учитывая, что это имеет ту же ошибку, что и другое целочисленное решение для отрицательных myInt1значений, может быть проще сделать что-то вроде:

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

Это должно дать правильный результат при divиспользовании только целочисленных операций.

Причина, по которой это неправильно : -1 и -5 должны давать 1, это дает 0

РЕДАКТИРОВАТЬ (еще раз, с чувством):
оператор деления округляется до нуля; для отрицательных результатов это совершенно верно, поэтому только неотрицательные результаты нуждаются в корректировке. Также учитывая, DivRemчто в любом случае выполняется a /и a %, давайте пропустим вызов (и начнем с простого сравнения, чтобы избежать вычисления по модулю, когда оно не нужно):

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

Причина, по которой это неправильно : -1 и 5 должны давать 0, это дает 1

(В свою защиту последней попытки я никогда не должен был пытаться дать обоснованный ответ, пока мой разум говорил мне, что я опоздал на 2 часа)


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.
Тимви

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 Вы знакомы с концепцией повторного использования кода ? oO
Rushyo

4

Вы можете использовать что-то вроде следующего.

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

12
Этот код явно неверен в двух отношениях. Во-первых, в синтаксисе есть небольшая ошибка; вам нужно больше скобок. Но что еще более важно, это не вычисляет желаемый результат. Например, попробуйте протестировать с a = -1000000 и b = 3999. Результат обычного целочисленного деления равен -250. Двойное деление -250.0625 ... Желаемое поведение - округлить. Очевидно, что правильное округление с -250.0625 должно округлять до -250, но ваш код округляется до -249.
Эрик Липперт

36
Мне жаль повторять это, но твой код все еще НЕПРАВИЛЬНО, Даниэль. 1/2 должно округлять до 1, но ваш код округляет до 0. Каждый раз, когда я нахожу ошибку, вы «исправляете» ее, вводя другую ошибку. Мой совет: прекратите это делать. Когда кто-то находит ошибку в вашем коде, не просто соединяйте исправления, не продумав ясно, в чем причина ошибки. Используйте хорошие инженерные практики; найдите ошибку в алгоритме и исправьте ее. Недостаток во всех трех неправильных версиях вашего алгоритма состоит в том, что вы не правильно определяете, когда округление было «вниз».
Эрик Липперт

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

5
Этот ответ, вероятно, является худшим, что я написал на SO ... и теперь он связан с блогом Эрика ... Ну, я не хотел дать удобочитаемое решение; Я действительно собирался быстро и быстро взломать. И чтобы снова отстаивать свое решение, я впервые понял идею правильно, но не думал о переполнении. Очевидно, что моей ошибкой было опубликовать код без написания и тестирования в VisualStudio. «Исправления» еще хуже - я не осознавал, что это проблема переполнения, и подумал, что допустил логическую ошибку. В результате первые «исправления» ничего не изменили; Я просто перевернул
Даниэль Брюкнер

10
логика и подтолкнул ошибку вокруг. Здесь я сделал следующие ошибки; как уже упоминал Эрик, я на самом деле не анализировал ошибку и просто делал первое, что казалось правильным. И я до сих пор не использовал VisualStudio. Ладно, я торопился и не потратил больше пяти минут на «починку», но это не должно быть оправданием. После того, как Эрик неоднократно указывал на ошибку, я запустил VisualStudio и обнаружил реальную проблему. Исправление с использованием Sign () делает вещь еще более нечитаемой и превращает ее в код, который вы не хотите поддерживать. Я усвоил урок и больше не буду недооценивать, насколько хитрым
Даниэль Брюкнер

-2

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

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

Для подписанных intts это не будет правильным


Не то, что ОП спросил в первую очередь, на самом деле не добавляет других ответов.
Сантамано

-4

Проблема со всеми решениями здесь либо в том, что они нуждаются в приведении, либо в числовой проблеме. Кастинг на float или double всегда возможен, но мы можем добиться большего.

Когда вы используете код ответа от @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.