【问题标题】:How can I ensure that a division of integers is always rounded up?如何确保整数的除法总是四舍五入?
【发布时间】:2009-05-28 14:37:42
【问题描述】:

如果需要,我想确保整数的除法总是向上取整。还有比这更好的方法吗?有很多铸造正在进行。 :-)

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

【问题讨论】:

  • 你能更清楚地定义你认为“更好”的东西吗?快点?更短?更准确的?更健壮?更明显正确?
  • 你总是在 C# 中进行大量的数学转换——这就是为什么它不是一种很好的语言来处理这类事情。您想要将值向上舍入还是远离零 - 应该 -3.1 变为 -3(向上)或 -4(远离零)
  • Eric:“更准确?更稳健?更明显正确?”是什么意思?其实我的意思只是“更好”,我会让读者更好地理解。所以如果有人有更短的代码,很好,如果另一个人有更快的,也很好:-)你有什么建议吗?
  • 这个问题竟然如此微妙地困难,而且讨论的启发性如此之大,真是令人惊讶。

标签: c# math


【解决方案1】:

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


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

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

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

  1. 除法将结果向零舍入

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

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

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

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

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

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

  2. 如果被除数为 int.minval 且除数为 -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;
}

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

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

【讨论】:

  • 优秀的模范答案
  • 我关心的不是行为;任何一种行为似乎都是合理的。我关心的是它没有指定,这意味着它不容易被测试。在这种情况下,我们定义了自己的操作符,因此我们可以指定我们喜欢的任何行为。我不在乎这种行为是“投掷”还是“不投掷”,但我确实关心它是否被声明。
  • 伙计 - 你能写一本书吗?
  • 这让我感到震惊——Jon Skeet..错了?无论如何,很好的答案。
  • @finnw:我是否测试过无关紧要。解决这个整数算术问题不是我的业务问题;如果是的话,我会测试它。如果有人想从互联网上获取陌生人的代码来解决他们的业务问题,那么他们有责任对其进行彻底测试。
【解决方案2】:

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

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

( dividend + divisor - 1 ) / divisor 

来源:Number Conversion, Roland Backhouse, 2001

【讨论】:

  • 嗯...那么除数=4,除数=(-2)? 4 / (-2) = (-2) = (-2) 四舍五入后。但是你提供的算法 (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0.5) = 0 在被四舍五入之后。
  • @Scott - 抱歉,我没有提到这个解决方案只适用于正除数和除数。我已经更新了我的答案以澄清这一点。
  • 我喜欢它,当然,作为这种方法的副产品,您可能会在分子中产生一些人为的溢出......
  • @PIntag:这个想法很好,但是模数的使用错误。取 13 和 3。预期结果为 5,但 ((13-1)%3)+1) 给出的结果为 1。采用正确的除法,1+(dividend - 1)/divisor 给出的结果与正除数和除数的答案相同。此外,没有溢出问题,无论它们是多么人为的。
  • @LutzL:感谢您对我的帮助,帮助您改进答案,但我承认我的“改进”是一个令人尴尬的失败。感谢您在这件事上的仁慈和外交 - 为此 - vielen dank。
【解决方案3】:

基于 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),但我们想要四舍五入。这意味着否定答案已经正确四舍五入,但肯定答案需要调整。

非零肯定答案很容易检测到,但答案为零有点棘手,因为它可以是负值的向上舍入或正值的向下舍入。

最安全的选择是通过检查两个整数的符号是​​否相同来检测答案何时应该是肯定的。在这种情况下,两个值的整数异或运算符“^”将导致符号位为 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 小时睡觉时尝试了一个合理的答案)

【解决方案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);

【讨论】:

  • 呃,什么?您的代码将被称为 myInt.DivideByAndRoundUp() 并且总是返回 1 除了输入 0 会导致异常...
  • 史诗般的失败。 (-2).DivideByAndRoundUp(2) 返回 0。
  • 我真的迟到了,但是这段代码可以编译吗?我的 Math 类不包含接受两个参数的 Ceiling 方法。
【解决方案5】:

你可以写一个助手。

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

【讨论】:

  • 仍然进行相同数量的投射
  • @Outlaw,随心所欲。但对我来说,如果他们不提出问题,我通常认为他们没有考虑过。
  • 如果只是这样,写一个助手是没有用的。相反,编写一个带有综合测试套件的助手。
  • @dolmen 你熟悉code reuse的概念吗? o.O
【解决方案6】:

你可以使用类似下面的东西。

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

【讨论】:

  • 这段代码显然有两个错误。首先,语法上有一个小错误;你需要更多的括号。但更重要的是,它不会计算所需的结果。例如,尝试使用 a=-1000000 和 b = 3999 进行测试。常规整数除法结果为 -250。双除法是-250.0625 ...所需的行为是四舍五入。显然,从 -250.0625 向上取整的正确方法是向上取整到 -250,但您的代码向上取整到 -249。
  • 很抱歉不得不一直这么说,但您的代码仍然是错误的 Daniel。 1/2 应该向上舍入为 1,但您的代码会将其向下舍入为 0。每次我发现一个错误时,您都会通过引入另一个错误来“修复”它。我的建议:停止这样做。当有人在您的代码中发现错误时,不要只是拼凑一个修复程序,而没有首先弄清楚是什么导致了错误。使用良好的工程实践;找出算法中的缺陷并修复它。您的算法的所有三个不正确版本的缺陷是您没有正确确定舍入“向下”的时间。
  • 难以置信的是,在这小段代码中会有多少错误。我从来没有太多时间去考虑它——结果在 cmets 中得到体现。 (1) a * b > 0 如果没有溢出就是正确的。 a 和 b 的符号有 9 种组合 - [-1, 0, +1] x [-1, 0, +1]。我们可以忽略 b == 0 的情况,留下 6 种情况 [-1, 0, +1] x [-1, +1]。 a / b 向零舍入,即四舍五入为负结果,向下舍入为正结果。因此,如果 a 和 b 具有相同的符号且不都是零,则必须执行调整。
  • 这个答案可能是我在 SO 上写的最糟糕的事情......现在它被 Eric 的博客链接了......好吧,我的意图不是给出一个可读的解决方案;我真的锁定了一个简短而快速的破解。为了再次捍卫我的解决方案,我第一次得到了正确的想法,但没有考虑溢出。显然我的错误是发布代码而不在 VisualStudio 中编写和测试它。 “修复”更糟糕——我没有意识到这是一个溢出问题,并认为我犯了一个逻辑错误。结果,第一个“修复”并没有改变任何东西。我只是倒置了
  • 逻辑并推动了错误。在这里,我犯了下一个错误;正如 Eric 已经提到的,我并没有真正分析这个错误,只是做了第一件看起来正确的事情。而且我仍然没有使用 VisualStudio。好吧,我很着急,没有在“修复”上花费超过五分钟,但这不应该成为借口。在我 Eric 反复指出错误后,我启动了 VisualStudio,发现了真正的问题。使用 Sign() 的修复使事情变得更加不可读,并将其变成您不想维护的代码。我吸取了教训,不会再低估它的棘手程度
【解决方案7】:

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

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。模数是相同的,但您改为添加除数。地板是一样的,但你在相反的条件下减去。

【讨论】:

    【解决方案8】:

    我认为你的意思是从零开始。没有任何铸件,使用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;
    }
    

    【讨论】:

      【解决方案9】:

      上面的一些答案使用浮点数,这是低效的,真的没有必要。对于无符号整数,这是 int1/int2 的有效答案:

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

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

      【讨论】:

      • 不是 OP 一开始就问的,并没有真正添加到其他答案中。
      【解决方案10】:

      这里所有解决方案的问题是他们需要演员或他们有一个数字问题。转换为 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 时,我们需要找到一种方法进行四舍五入”——你错过了这个问题的重点——或者总是四舍五入,即 OP想要将 0.001 向上舍入为 1。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-06
      • 1970-01-01
      相关资源
      最近更新 更多