关于c#:我怎样才能确保整数除法总是四舍五入?

How can I ensure that a division of integers is always rounded up?

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

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


更新:这个问题是我2013年1月博客的主题。谢谢你的提问!

求整数算术正确是困难的。到目前为止,已经充分证明,当你尝试做一个"聪明"的把戏时,你犯了一个错误的几率是很高的。当发现一个缺陷时,在不考虑修复是否破坏其他东西的情况下,更改代码来修复缺陷并不是一种很好的解决问题的技术。到目前为止,我们已经有五个不同的不正确的整数算术解决方案,这完全不是特别困难的问题张贴。

处理整数算术问题的正确方法——也就是说,增加第一次得到正确答案的可能性的方法——是仔细地处理问题,一步一步解决问题,并使用良好的工程原理。

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

  • 除法将结果四舍五入为零。

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

  • 如果左操作数是最小的可表示整数,而右操作数是-1,则会发生溢出。[…]它是一种实现,定义为[算术异常]是被抛出还是溢出未被报告,结果值是左操作数的值。

  • 如果右操作数的值为零,则引发System.DivideByZeroException。

  • 我们需要的是一个整数除函数,它计算商,但总是向上舍入结果,而不是总是向零。

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

  • 除数为零时运算抛出

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

  • 如果没有余数——除法是"偶数"——则返回值是整数商。

  • 否则,它返回大于商的最小整数,也就是说,它总是向上取整。

  • 现在我们有了一个规范,所以我们知道我们可以提出一个可测试的设计。假设我们添加了一个额外的设计标准,即问题仅用整数算术来解决,而不是将商计算为双精度,因为"双精度"解决方案已在问题语句中被明确拒绝。

    那么我们必须计算什么呢?很明显,为了满足我们的规范,同时只保留整数算术,我们需要知道三个事实。首先,整数商是什么?第二,除法是否没有余数?第三,如果不是,整数商是通过向上取整还是向下取整来计算的?

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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;
    }

    这很聪明吗?不漂亮吗?没有?不,根据规格正确吗?我相信,但我还没有完全测试过。不过看起来不错。

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


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

    在C和Java中,对于积极的分红和除数,你只需要做:

    1
    ( dividend + divisor - 1 ) / divisor

    资料来源:数字转换,罗兰·巴克豪斯,2001年


    基于int的最终答案

    对于有符号整数:

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

    对于无符号整数:

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

    这个答案的理由

    整数除法"/被定义为向零取整(规范的7.7.2),但我们希望向上取整。这意味着否定的答案已经正确地四舍五入,但肯定的答案需要调整。

    非零正答案很容易被发现,但答案零有点难,因为它可以是负值的四舍五入,也可以是正值的四舍五入。

    最安全的赌注是通过检查两个整数的符号是否相同来检测什么时候答案应该是正的。两个值上的整数xor运算符"EDOCX1"〔1〕在这种情况下将导致0符号位,这意味着非负结果,因此检查(a ^ b) >= 0确定结果应在舍入前为正。还要注意,对于无符号整数,每个答案显然是正的,因此可以省略此检查。

    唯一剩下的检查就是是否进行了取整,a % b != 0将对此进行检查。

    经验教训

    算术(整数或其他)并不像看起来那么简单。任何时候都需要仔细思考。

    另外,尽管我的最终答案可能不像浮点数答案那样简单、明显,或者甚至是快速,但它对我来说有一个很强的补偿性质;我现在已经通过答案进行了推理,因此我实际上确信它是正确的(除非有人更聪明地告诉我,否则,偷看一下埃里克的方向-)。

    为了对浮点数的答案有同样的确定感,我必须做更多(可能更复杂)的事情,考虑是否存在任何条件,在这种情况下,浮点数精度可能会阻碍,以及Math.Ceiling是否可能会对"正确"的输入做一些不可取的事情。

    走过的路

    替换(注i将第二个myInt1替换为myInt2,假设这是您的意思):

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

    用:

    1
    (myInt1 - 1 + myInt2) / myInt2

    唯一需要注意的是,如果myInt1 - 1 + myInt2溢出所使用的整数类型,可能无法获得预期的结果。

    这是错误的原因:-1000000和3999应该给出-250,这给出-249

    编辑:考虑到这与其他针对负myInt1值的整数解的错误相同,可能更容易执行以下操作:

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

    这将给出仅使用整数运算的div的正确结果。

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

    编辑(再一次,带着感觉):除法运算符向零旋转;对于否定结果,这是完全正确的,因此只有非负结果需要调整。还考虑到EDCOX1〔10〕只做EDCOX1〔0〕和EDCOX1〔12〕,无论如何,让我们跳过呼叫(开始时用简单的比较来避免当不需要时进行模运算):

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

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

    (在我为最后一次尝试辩护时,我不应该尝试一个合理的答案,而我的大脑告诉我我睡了2个小时)


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

    1
    2
    3
    4
    5
    6
    7
    public static class Int32Methods
    {
        public static int DivideByAndRoundUp(this int number, int divideBy)
        {                        
            return (int)Math.Ceiling((float)number / (float)divideBy);
        }
    }

    这也使代码更具可读性:

    1
    int result = myInt.DivideByAndRoundUp(4);


    你可以写一个助手。

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


    您可以使用如下内容。

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


    这里所有的解的问题是,要么它们需要一个强制转换,要么它们有一个数值问题。铸造浮动或双重总是一个选择,但我们可以做得更好。

    当您使用@jerryjvl的答案代码时

    1
    2
    3
    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%时才会进行舍入。所以调整后的代码如下:

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

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