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

Kar*_*ten 235 c# math

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

(int)Math.Ceiling((double)myInt1 / myInt2)
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 662

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


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

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

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

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

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

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

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

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

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

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

  2. 如果dividend为int.minval且divisor为-1,则抛出操作

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

  4. 否则,它返回大于商的最小整数,也就是说,它总是向上舍入.

现在我们有一个规格,所以我们知道我们可以提出一个可测试的设计.假设我们添加了一个额外的设计标准,即只用整数算法解决问题,而不是将商计算为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;
}
Run Code Online (Sandbox Code Playgroud)

这个聪明吗?不,很美?不,短?没有.根据规格正确吗?我相信,但我还没有完全测试过它.虽然它看起来很不错.

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

  • @finnw:我是否测试过它是无关紧要的.解决这个整数算术问题不是*我的*业务问题; 如果是的话,我会测试它.如果有人想从互联网上的陌生人那里获取代码来解决他们的业务问题,那么他们有责任对他们进行彻底的测试. (75认同)
  • Darn it,pedantry失败:( (68认同)
  • 我关心的不是行为; 这两种行为都是合理的.我关心的是它不是_specified_,这意味着它不容易被测试.在这种情况下,我们正在定义自己的运算符,因此我们可以指定我们喜欢的任何行为.我不在乎这种行为是"扔"还是"不扔",但我确实在意它. (61认同)
  • 优秀的模范答案 (44认同)
  • 男人 - 你能写一本书吗? (32认同)
  • 这震撼了我的核心 - Jon Skeet ......错了?无论如何,好的答案. (23认同)
  • 太棒了.通过这个答案我学到了很多关于算法设计的知识. (22认同)
  • @Jon:DivRoundUp(12,2)属于规范的情况(3),而不是情况(4). (18认同)
  • 我认为这是整个SO的最佳答案之一. (16认同)
  • @EricLippert"我们这里的专业人士"把我的迂回帽戴上(而且非常精湛):Stack Overflow不仅适用于专业人士,也适用于业余爱好者.所以这个陈述是不正确的,即使这决不会削弱你的实际观点.哎呀,现在我已经把它放在胸前,我可以在晚上睡觉. (6认同)
  • 我打赌@EricLippert需要一个星期的时间来写一个你好世界的应用程序,但它将是那里最好的该死的你好世界应用程序 (6认同)
  • 我怀疑这个答案的前两句可以应用于大量的主题,将"整数算术"替换为主题的名称.作为一个迂腐的问题,我还考虑将规范从"大于商"改为"大于或等于商".否则DivRoundUp(12,2)应该返回7,作为大于6的最小整数. (5认同)
  • 我看到了这个(很棒的答案),并想知道为什么Eric没有使用`Math.DivRem`而不是两次执行除法('/'和'%').然后我查看了Math.DivRem的源代码.它执行两次分裂.大多数微处理器都提供单个div指令的商和余数,但我想并非全部.如果已经在CLR中实现了Math.DivRem,那么它可以针对返回两个结果的处理器进行优化.令人失望. (5认同)
  • 我从VBA和MySQL pre 5.0中的四舍五入的问题中得知(我认为基于C实现舍入的方式存在问题的四舍五入问题)从来没有像在语言中实现的那样舍入(尽管它可能已经变得更好).我几乎总是写自己的方法或操作符,所以我确切地知道*在每种情况下都会有什么期望.做得好. (2认同)
  • @Tergiver或者抖动足够智能,它不需要额外的提示,并且认识到它可以减少除法和模数与单个指令相同的分母. (2认同)
  • Eric,而不是`bool wasRoundedDown =((divisor> 0)==(dividend> 0))`,你觉得`bool wasRoundedDown = Math.sign(divisor)== Math.sign(dividend)`读得更好沟通上面评论中提到的逻辑?就是想. (2认同)
  • 为什么你不只是使用余数来确定它被舍入的方向?`bool wasRoundedDown =(dividend%divisor)> 0;` (2认同)
  • 上面的程序*是*漂亮的。我们不应该认为美是简洁或复杂的——我们应该将美视为具有启发性的。只有一个很好的答案的问题:)。 (2认同)

jer*_*jvl 48

最终的基于int的答案

对于有符号整数:

int div = a / b;
if (((a ^ b) >= 0) && (a % b != 0))
    div++;
Run Code Online (Sandbox Code Playgroud)

对于无符号整数:

int div = a / b;
if (a % b != 0)
    div++;
Run Code Online (Sandbox Code Playgroud)

这个答案的原因

整数除法' /'被定义为舍入为零(规范的7.7.2),但我们想要四舍五入.这意味着否定答案已经正确舍入,但需要调整肯定答案.

非零肯定答案很容易被发现,但是零回答有点棘手,因为这可能是负值的四舍五入或者是正值的四舍五入.

最安全的选择是通过检查两个整数的符号是​​否相同来检测答案何时应为正.^在这种情况下,对两个值的整数xor运算符' '将导致0符号位,这意味着非负结果,因此检查(a ^ b) >= 0确定在舍入之前结果应为正.另请注意,对于无符号整数,每个答案显然都是正数,因此可以省略此检查.

剩下的唯一检查是否发生任何舍入,为此a % b != 0将完成工作.

得到教训

算术(整数或其他)并不像看起来那么简单.始终谨慎思考.

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

为了得到浮点答案的确定性,我必须做更多(可能更复杂)的思考是否存在浮点精度可能妨碍的任何条件,以及是否Math.Ceiling可能在'恰到好处'的投入上不受欢迎的东西.

走过的路

更换(注意我更换了第二myInt1myInt2,假设这是你的意思):

(int)Math.Ceiling((double)myInt1 / myInt2)
Run Code Online (Sandbox Code Playgroud)

有:

(myInt1 - 1 + myInt2) / myInt2
Run Code Online (Sandbox Code Playgroud)

唯一需要注意的是,如果myInt1 - 1 + myInt2溢出您正在使用的整数类型,您可能无法得到您期望的结果.

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

编辑:
考虑到这与负值的其他整数解决方案具有相同的错误myInt1,可能更容易做类似的事情:

int rem;
int div = Math.DivRem(myInt1, myInt2, out rem);
if (rem > 0)
  div++;
Run Code Online (Sandbox Code Playgroud)

这应该div只使用整数运算给出正确的结果.

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

编辑(再一次,有感觉):
除法运算符向零舍入; 对于负面结果,这是完全正确的,因此只有非负面结果需要调整.还考虑到DivRem只做了a /和a %,让我们跳过调用(从简单的比较开始,以避免在不需要时进行模数计算):

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;
Run Code Online (Sandbox Code Playgroud)

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

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


Ian*_*son 47

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

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

( dividend + divisor - 1 ) / divisor 
Run Code Online (Sandbox Code Playgroud)

资料来源:编号转换,Roland Backhouse,2001年

  • 嗯...那么被除数=4,除数=(-2)呢???4 / (-2) = (-2) = (-2) 向上舍入后。但您提供的算法在向上舍入后为 (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0.5) = 0 。 (2认同)
  • @Scott - 抱歉,我忽略了这个解决方案仅适用于正股息和除数。我已经更新了我的答案以澄清这一点。 (2认同)
  • @PIntag:这个想法很好,但使用模数错误.取13和3.预期结果5,但是`((13-1)%3)+ 1)`给出1作为结果.采用正确的除法,"1+(被除数 - 1)/除数"给出与正被除数和除数的答案相同的结果.此外,没有溢出问题,无论它们是多么人为的. (2认同)

jos*_*ley 20

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

public static class Int32Methods
{
    public static int DivideByAndRoundUp(this int number, int divideBy)
    {                        
        return (int)Math.Ceiling((float)number / (float)divideBy);
    }
}
Run Code Online (Sandbox Code Playgroud)

这使得您的代码也可以读取:

int result = myInt.DivideByAndRoundUp(4);
Run Code Online (Sandbox Code Playgroud)

  • 史诗般的失败.(-2).DivideByAndRoundUp(2)返回0. (5认同)
  • 我参加派对的时间已经很晚了,但这段代码是否已编译?我的Math类不包含带有两个参数的Ceiling方法. (3认同)

Jar*_*Par 17

你可以写一个帮手.

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}
Run Code Online (Sandbox Code Playgroud)

  • @Outlaw,假设你想要的一切.但对我来说,如果他们没有提出问题,我通常认为他们没有考虑它. (28认同)
  • 据推测OP很聪明,已经把它放在一个功能中...... (11认同)
  • @dolmen你熟悉_code_ _reuse_的概念吗?OO (2认同)