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


242

我想确保在必要时总是对整数除法进行四舍五入。有没有比这更好的方法了?正在进行很多强制转换。:-)

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

48
您能否更清楚地定义您认为“更好”的东西?快点?更短?更准确?更健壮?更明显地正确吗?
埃里克·利珀特

6
您总是使用C#进行大量的数学转换-这就是为什么它不是这种语言的好语言。您是否希望将值四舍五入或从零开始-应该将-3.1转到-3(向上)还是-4(从零开始)
Keith,2009年

9
埃里克(Eric):“更准确?更健壮?更明显正确?”是什么意思?实际上,我的意思只是“更好”,我希望读者能更好地理解。因此,如果某人的代码较短,那就太好了;如果另一个人的代码速度更快,也就好了,:-)您对此有何建议?
卡斯滕

1
我是唯一一个读了标题的人,“哦,这是C#的摘要”?
马特·鲍尔

6
真令人惊奇的是,这个问题有多么微妙,讨论变得多么有启发性。
贾斯汀·摩根

Answers:


669

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


要使整数算术正确,是很难的。到目前为止,已经充分证明了,当您尝试做一个“聪明”的窍门时,很可能您犯了一个错误。而且,当发现缺陷时,更改代码以修正缺陷而不考虑修正是否破坏了其他问题并不是一种好的解决问题的技术。到目前为止,我已经发布了五个完全不特别困难的问题的五个不同的不正确的整数算术解决方案。

解决整数算术问题的正确方法(即增加首次获得正确答案的可能性的方法)是认真解决问题,一次解决一个问题,并在执行过程中使用良好的工程原理所以。

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

  1. 除法将结果舍入为零

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

  3. 如果左操作数是可表示的最小int,而右操作数是–1,则发生溢出。[...]由实现定义,是抛出[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;
}

这很聪明吗?不,美丽吗?不,简短吗?否。根据规格正确吗?我相信,但是我还没有完全测试它。看起来不错。

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


44
出色的典范答案
加文·米勒

61
我所关心的不是行为,而是行为。两种行为似乎都是合理的。我关心的是未指定,这意味着无法轻松对其进行测试。在这种情况下,我们将定义自己的运算符,因此我们可以指定所需的任何行为。我不在乎该行为是“抛出”还是“不要抛出”,但我确实在意它的陈述。
埃里克·利珀特

68
该死,学步失败:(
乔恩·斯基特

32
男人-请问你能写一本书吗?
xtofl

76
@finnw:是否进行测试无关紧要。解决这个整数算术问题不是我的业务问题。如果是这样,那我将对其进行测试。如果有人想从互联网上获取陌生人的代码来解决他们的业务问题,那么他们有责任对它进行彻底的测试。
埃里克·利珀特

49

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

在C#和Java中,为获得正除数和除数,您只需要执行以下操作:

( dividend + divisor - 1 ) / divisor 

资料来源:数字转换,罗兰·巴克豪斯(Roland Backhouse),2001


太棒了 虽然,您应该添加括号以消除歧义,但它的证明有点冗长,但是您只需看一下就可以在肠道中感觉到它是正确的。
约尔根Sigvardsson

1
嗯...股息= 4,除数=(-2)呢?四舍五入后的4 /(-2)=(-2)=(-2)但四舍五入后,您提供的算法(4 +(-2)-1)/(-2)= 1 /(-2)=(-0.5)= 0。
斯科特,

1
@Scott-抱歉,我没有提及此解决方案仅适用于正股利和除数的情况。我已更新我的答案,以提清此问题。
伊恩·纳尔逊

1
我喜欢它,当然,作为这种方法的副产品,分子中可能会有一些人为的溢出……
TCC

2
@PIntag:这个主意不错,但是使用模数是错误的。取13和3。预期结果为5,但((13-1)%3)+1)结果为1。进行正确的除法1+(dividend - 1)/divisor运算,得出的结果与正数除数和除数的答案相同。而且,没有溢出问题,无论它们可能是人为的。
Lutz Lehmann 2014年

48

最终的基于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可能呢在“恰到好处”的输入上有些不受欢迎的东西。

走过的路

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

(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

(以我自己为上一次尝试的辩护,在我的头脑告诉我我睡了两个小时之后,我不应该尝试一个合理的答案)


19

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

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

1
恩,什么?您的代码将被称为myInt.DivideByAndRoundUp(),并且始终会返回1,但输入0除外,这会导致异常...
配置器

5
史诗般的失败。(-2).DivideByAndRoundUp(2)返回0。–
Timwi,

3
我参加聚会真的很晚,但是此代码可以编译吗?我的Math类不包含带有两个参数的Ceiling方法。
R. Martinho Fernandes

17

您可以编写一个助手。

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

1
尽管如此铸造事情,虽然同量
ChrisF

29
@Outlaw,假设所有您想要的。但是对我来说,如果他们不提出这个问题,我通常会认为他们没有考虑这个问题。
JaredPar

1
仅仅是写一个助手是没有用的。而是编写带有全面测试套件的帮助程序。
支石墓

3
@dolmen您熟悉代码 重用的概念吗?oO
Rushyo,2012年

4

您可以使用类似以下的内容。

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

12
这段代码显然有两种错误。首先,语法有一个小错误;您需要更多的括号。但更重要的是,它无法计算出预期的结果。例如,尝试使用a = -1000000和b = 3999进行测试。常规整数除法结果为-250。双除法是-250.0625 ...所需的行为是向上舍入。显然,从-250.0625进行的正确舍入是将其舍入到-250,但是您的代码将舍入到-249。
埃里克·利珀特

36
很抱歉不得不这么说,但是您的代码仍然是Daniel。1/2应该向上舍入为1,但是您的代码会将其舍入为0。每次我发现错误时,都通过引入另一个错误来“修复”它。我的建议:停止这样做。当某人在您的代码中发现错误时,不要只是打个补丁,而不必首先仔细考虑是什么原因导致了该错误。采用良好的工程实践;找到算法中的缺陷并进行修复。算法的所有三个错误版本的缺陷在于,您无法正确确定舍入的时间。
埃里克·利珀特

8
太少了,这小小的代码中有多少bug。我从来没有太多时间考虑它-结果体现在评论中。(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具有相同的符号并且都不都是零,则必须执行调整。
丹尼尔·布鲁克纳(DanielBrückner),2009年

5
这个答案可能是我在SO上写的最糟糕的东西...,现在已经由Eric的博客链接了...嗯,我的目的不是要提供一个可读的解决方案;我真的锁定了一个简短而快速的技巧。为了再次捍卫我的解决方案,我第一次想到了正确的主意,但没有考虑溢出问题。显然,发布代码而不在Visual Studio中编写和测试代码是我的错误。“修复”甚至更糟-我没有意识到这是一个溢出问题,并认为我犯了一个逻辑错误。结果,最初的“修复”并没有改变任何东西。我只是倒了
DanielBrückner,2009年

10
逻辑并推销错误。在这里,我犯了下一个错误。正如埃里克(Eric)已经提到的那样,我并没有真正分析该错误,只是做了第一件事。而且我仍然没有使用VisualStudio。好的,我很着急,没有花超过五分钟的时间进行“修复”,但这不应成为借口。在Eric反复指出该错误之后,我启动了VisualStudio,并找到了真正的问题。使用Sign()进行的修复使事情变得更加不可读,并将其转变为您真正不想维护的代码。我吸取了教训,不会再低估技巧了
DanielBrückner2009年

-2

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

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

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


并非OP首先提出的要求,并没有真正增加其他答案。
santamanno

-4

这里所有解决方案的问题在于它们是否需要强制转换或存在数值问题。强制浮起或翻倍始终是一种选择,但我们可以做得更好。

当您使用@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。
Grhm 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.