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

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

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

Ответ 1

UPDATE: этот вопрос был тема моего блога в январе 2013 года. Спасибо за отличный вопрос!


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

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

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

  • Деление округляет результат к нулю

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

  • Если левый операнд является наименьшим представимым int, а правый операнд равен -1, происходит переполнение. [...] определяется реализацией того, выбрано ли [Арифметическое исключение] или переполнение не отображается, а результирующее значение имеет значение в левом операнде.

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

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

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

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

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

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

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

Ответ 2

Окончательный ответ на основе 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 спецификации), но мы хотим округлить. Это означает, что отрицательные ответы уже округлены правильно, но необходимо настроить положительные ответы.

Легко обнаружить ненулевые положительные ответы, но ответ ноль немного сложнее, поскольку это может быть либо округление отрицательного значения, либо округление положительного.

Самая безопасная ставка заключается в том, чтобы определить, когда ответ должен быть положительным, если проверить, что знаки обоих целых чисел идентичны. Оператор Integer 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

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

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

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

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

Ответ 3

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

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

( dividend + divisor - 1 ) / divisor 

Источник: Преобразование чисел, Roland Backhouse, 2001

Ответ 4

Отличная возможность использовать метод расширения:

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);

Ответ 5

Вы можете написать помощника.

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

Ответ 6

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

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

Ответ 7

Проблема со всеми решениями здесь заключается в том, что им нужен актерский состав или у них есть численная проблема. Кастинг для 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. Результат оператора modulo в верхнем примере имеет диапазон от 0 до myInt2-1. Округление будет происходить только в том случае, если остаток превышает 50% от делителя. Таким образом, скорректированный код выглядит следующим образом:

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

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