如何确保整数除法总是四舍五入?

时间:2009-05-28 14:37:42

标签: c# math

我想确保在必要时整数除以整数。有比这更好的方法吗?有很多铸件正在进行中。 : - )

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

10 个答案:

答案 0 :(得分:658)

更新:这个问题是the subject of my blog in January 2013。谢谢你提出的好问题!


使整数算术正确是很难的。正如迄今为止已经充分证明的那样,当你尝试做一个“聪明”的伎俩时,你犯错误的可能性很大。当发现一个缺陷时,更改代码以修复缺陷而不考虑修复是否会破坏其他东西并不是一个很好的解决问题的技巧。到目前为止,我们已经考虑了五种不同的错误整数算术解决方案来解决这个完全不是特别困难的问题。

解决整数算术问题的正确方法 - 即增加第一次得到正确答案的可能性的方法 - 是仔细解决问题,一次一步地解决问题,并使用良好的工程这样做的原则。

首先阅读您要替换的内容的规范。整数除法的规范明确指出:

  1. 该部门将结果舍入为零

  2. 当两个操作数具有相同符号时,结果为零或为正;当两个操作数具有相反符号时,结果为零或为负

  3. 如果左操作数是最小的可表示的int而右操作数是-1,则发生溢出。 [...]它是实现定义的,是否抛出[ArithmeticException]或溢出未报告,结果值是左操作数的值。

  4. 如果右操作数的值为零,则抛出System.DivideByZeroException。

  5. 我们想要的是一个整数除法函数,它计算商但是将结果向上舍入,而不是总是向零

    所以为该函数编写规范。我们的函数int DivRoundUp(int dividend, int divisor)必须为每个可能的输入定义行为。这种未定义的行为令人深感担忧,所以让我们消除它。我们会说我们的操作有这个规范:

      如果除数为零,则
    1. 操作抛出

    2. 如果被除数为int.minval且除数为-1,则
    3. 操作抛出

    4. 如果没有余数 - 除法是'偶数' - 那么返回值就是整数商

    5. 否则它会返回大于最小整数,也就是说,它总是向上舍入。

    6. 现在我们有了规范,所以我们知道我们可以提出可测试的设计。假设我们添加了一个额外的设计标准,即只用整数算法解决问题,而不是将商计算为double,因为在问题陈述中明确拒绝了“双”解决方案。

      那么我们必须计算什么?显然,为了满足我们的规范,同时只保留整数算术,我们需要知道三个事实。首先,什么是整数商?第二,没有剩余部门吗?第三,如果没有,是通过向上或向下舍入计算的整数商?

      既然我们有规范和设计,我们就可以开始编写代码了。

      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;
      }
      
      这是聪明的吗?不,很美?不,短?没有。根据规格正确吗? 我相信,但我还没有完全测试过它。虽然它看起来很不错。

      我们是这里的专业人士;使用良好的工程实践。研究您的工具,指定所需的行为,首先考虑错误情况,然后编写代码以强调其明显的正确性。当您发现错误时,请考虑您的算法是否在您之前开始时存在严重缺陷只是随机地开始交换比较的方向并打破已经有效的东西。

答案 1 :(得分:45)

最终的基于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将完成此任务。

经验教训

算术(整数或其他)并不像看起来那么简单。始终认真考虑。

此外,虽然我的最终答案可能不像浮点回答的那样“简单”或“明显”或甚至“快速”,但它对我来说具有非常强大的救赎品质;我现在已经通过答案推理了,所以我确定它是正确的(直到有人更聪明地告诉我 - 偷偷地看着Eric的方向 - )。

为了得到浮点答案的确定性,我必须做更多(可能更复杂)的思考是否存在浮点精度可能会妨碍的方式,以及是否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只做一个/%,让我们跳过这个调用(从简单比较开始,避免在不需要时进行模数计算):

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

原因这是错误的:-1和5应该给0,这给出了1

(在我自己的最后一次尝试的辩护中,我的心灵告诉我睡眠时间已经晚了2个小时,我不应该尝试一个合理的答案)

答案 2 :(得分:45)

到目前为止,所有答案似乎都过于复杂了。

在C#和Java中,对于正红利和除数,您只需要这样做:

( dividend + divisor - 1 ) / divisor 

来源:Number Conversion, Roland Backhouse, 2001

答案 3 :(得分:20)

使用扩展方法的绝佳机会:

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

答案 4 :(得分:18)

你可以写一个帮手。

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

答案 5 :(得分:5)

您可以使用以下内容。

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

答案 6 :(得分:0)

对于有符号或无符号整数。

q = x / y + !(((x < 0) != (y < 0)) || !(x % y));

对于有符号的被除数和无符号的除数。

q = x / y + !((x < 0) || !(x % y));

对于无符号股息和有符号除数。

q = x / y + !((y < 0) || !(x % y));

对于无符号整数。

q = x / y + !!(x % y);

零除数失败(与本机操作一样)。

不能溢出。

优雅而正确。

理解行为的关键是识别截断、下限和上限划分的差异。 C#/C++ 被本机截断。当商为负(即运算符符号不同)时,截断是一个上限(负值较小)。否则截断是下限(不太积极)。

所以,如果有余数,如果结果为正则加1。 Modulo 是相同的,但您添加了除数。地板是一样的,但你在相反的条件下减去。

答案 7 :(得分:0)

四舍五入,我认为你的意思是永远远离零。没有任何强制转换,使用 Math.DivRem() 函数

/// <summary>
/// Divide a/b but always round up
/// </summary>
/// <param name="a">The numerator.</param>
/// <param name="b">The denominator.</param>
int DivRndUp(int a, int b)
{
    // remove sign
    int s = Math.Sign(a) * Math.Sign(b);
    a = Math.Abs(a);
    b = Math.Abs(b);
    var c = Math.DivRem(a, b, out int r);
    // if remainder >0 round up
    if (r > 0)
    {
        c++;
    }
    return s * c;
}

如果舍入意味着无论符号总是向上,则

/// <summary>
/// Divide a/b but always round up
/// </summary>
/// <param name="a">The numerator.</param>
/// <param name="b">The denominator.</param>
int DivRndUp(int a, int b)
{
    // remove sign
    int s = Math.Sign(a) * Math.Sign(b);
    a = Math.Abs(a);
    b = Math.Abs(b);
    var c = Math.DivRem(a, b, out int r);
    // if remainder >0 round up
    if (r > 0)
    {
        c+=s;
    }
    return s * c;
}

答案 8 :(得分:-2)

上面的某些答案使用浮点数,这效率低下,实际上是没有必要的。对于无符号整数,这是对int1 / int2的有效回答:

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

对于带符号的整数,这将是不正确的

答案 9 :(得分:-3)

这里所有解决方案的问题是他们需要演员或他们有数字问题。铸造浮动或双重始终是一种选择,但我们可以做得更好。

当您使用@jerryjvl

的答案代码时
int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

存在舍入错误。 1/5会向上舍入,因为1%5!= 0.但这是错误的,因为只有在用1替换1时才会发生舍入,因此结果为0.6。当计算给出大于或等于0.5的值时,我们需要找到一种向上舍入的方法。上部示例中的模运算符的结果范围从0到myInt2-1。只有当余数大于除数的50%时才会进行舍入。所以调整后的代码如下所示:

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

当然我们在myInt2 / 2上也有一个舍入问题,但是这个结果会给你一个比这个网站上更好的舍入解决方案。