我想确保整数的除法在必要时总是向上取整。有比这更好的方法吗?有很多铸件正在进行中。-)
1
| (int)Math.Ceiling((double)myInt1 / myInt2) |
- 你能更清楚地定义你认为"更好"的东西吗?更快?更短的?更准确?更稳健?更明显是正确的?
- 你总是有很多C语言的数学演员——这就是为什么它不是这类事情的好语言。您希望值向上舍入还是远离零-应该-3.1转到-3(向上)或-4(远离零)
- 埃里克:你说的"更准确"是什么意思?更稳健?更明显是正确的?"实际上,我的意思只是"更好",我会让读者更好地理解意思。所以,如果有人有一段更短的代码,很好,如果另一段代码更快,也很好:—)你有什么建议吗?
- 我是唯一一个读到书名的人吗,"哦,这是C的一种概括?"
- 整数除法结果的可能重复
- 令人惊讶的是,这个问题如此微妙的困难,以及讨论的指导性。
- 我必须同意贾斯汀的观点。这就是同行评审存在的原因。
更新:这个问题是我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,在你随机开始交换比较的方向并打破已经起作用的东西之前,先考虑一下你的算法是否有严重的缺陷。
- 出色的示范性回答
- ACK…我应该在写完我的文章之前先查一查。
- 您是否真的认为int.minvalue和-1的未指定行为是不受欢迎的,因为规范说它只发生在未检查的上下文中?当然,如果程序在未检查的上下文中执行一个除法,我们应该假定它们是专门试图避免异常的?
- 我关心的不是行为,两种行为似乎都是正当的。我关心的是它没有被指定,这意味着它不容易被测试。在这种情况下,我们定义了我们自己的操作符,所以我们可以指定我们喜欢的任何行为。我不在乎这种行为是"扔"还是"不扔",但我真的在乎它被陈述。
- 这是一个很好的观点。出于好奇,你知道为什么第二种情况可能在所有语言的规范中都没有明确规定吗?
- 我怀疑这个答案的前两个句子可以应用到大量的主题中,用主题的名称替换"整数算术"。作为一个迂腐的问题,我也会考虑将规格从"大于商"改为"大于或等于商"。否则,divRoundup(12,2)应返回7,作为大于6的最小整数。
- @乔恩:DivRoundup(12,2)属于规范的案例(3),而不是案例(4)。
- @Jerryjvl:我不确定,但我的猜测是,一些硬件使得检测这种情况变得便宜,而有些则不然。
- 该死,学究失败了:(
- 棒极了。通过这个答案,我学到了很多关于算法设计的知识。
- 你能写一本关于这个的书吗?
- 我从vba和mysql pre 5.0中的舍入问题(我都认为这是基于C实现舍入方式的问题而产生的舍入问题)中学习到了永远不要像语言中实现的那样舍入(尽管也许它已经变得更好了)。我几乎总是写自己的方法或运算符,所以我确切地知道在每种情况下会发生什么。做得好。
- 我迟到了-分析和解释得很好。
- 我认为这是整个社会的最佳答案之一。
- 这让我心潮澎湃-乔恩·斯基特…错了?无论如何,回答得很好。
- 这篇文章写得太棒了!!
- 我看到了这一点(奇妙的答案),我想知道为什么埃里克没有使用Math.DivRem,而不是执行两次除法("/"和"%"。然后我查看了math.divrem的源代码。它执行两次除法。大多数微处理器都从单一的DIV指令提供商数和余数,但我想不是全部。不过,如果Math.DivRem已经在CLR内部实现,那么它可以对返回两个结果的处理器进行优化。令人失望的。
- @ Tergiver或抖动可能是足够智能的,它不需要额外的提示,并且认识到它可以减少对同一分母与单个指令的除法和模数。
- 也许是吧。作为一个PasaMistor,我更倾向于相信人们只是变得懒惰。DeVeRM库方法的目的是提供机会将其优化为不支持多个返回值的语言的单个分区。现在看来,分裂并不是过去的野兽,除非你在循环中做很多事情,否则这不是什么大不了的事情。我的问题是:为什么要包括一个库方法,它的唯一目的是在没有任何优化时提供优化的机会?
- @芬诺:我是否测试过它是无关紧要的。解决这个整数算术问题不是我的业务问题;如果是,那么我将测试它。如果有人想从互联网上的陌生人那里获取代码来解决他们的业务问题,那么他们的责任就是彻底测试它。
- 埃里克,你认为bool wasRoundedDown = Math.sign(divisor) == Math.sign(dividend)比bool wasRoundedDown = ((divisor > 0) == (dividend > 0))读起来更好,以便传达上述评论中提到的逻辑吗?只是想知道。
- @解决方案:我想两者都可以。
- 很好的回答。我用的是无符号整数。我很确定可以跳过(int.minvalue/-1)的检查,也可以跳过向下取整的检查。同意吗?
- @埃里克利珀特的"我们是这里的专业人士",把我的书呆子的帽子戴上(这是非常刺耳的):栈溢出不仅仅是专业人士,它也是爱好者。所以这句话是不正确的,即使这不会降低你的实际观点。哎呀,现在我胸口有了这个,晚上就可以睡觉了。
- 为什么你不只用余数来确定它被取整的方向呢?bool wasRoundedDown = (dividend % divisor) > 0;
- @伊姆蒂坦:那将是一个完全合理的替代方案。
- 我打赌@ericlippert花了一周时间写一个Hello World应用程序,但它将是目前最好的Hello World应用程序。
- 上面的节目很漂亮。我们不应该认为美是简单或复杂的——我们应该把美看作是开明的。只有一个很好的答案:)。
- 模运算相对来说比较慢(在某些机器上,除非CPU像某些机器一样,将模运算和商作为单个操作的一部分输出,但编译器并不总是利用这一点)。在大多数情况下,计算roundedTowardsZeroQuotient * divisor == dividend比divisor % dividend == 0更有效。
到目前为止,所有的答案似乎都过于复杂了。
在C和Java中,对于积极的分红和除数,你只需要做:
1
| ( dividend + divisor - 1 ) / divisor |
资料来源:数字转换,罗兰·巴克豪斯,2001年
- 令人惊叹的。虽然,你应该加上括号,以消除歧义。它的证明有点冗长,但你可以感觉到,它是正确的,只要看看它。
- 添加了括号。
- 六羟甲基三聚氰胺六甲醚。。。股息=4,除数=(-2)怎么样????4/(-2)=(-2)=(-2)四舍五入后。但是,您提供的算法(4+(-2)-1/(-2)=1/(-2)=0经过四舍五入后。
- @斯科特-对不起,我没有提到这个解决方案只适用于正股息和除数。我已经更新了我的答案,以说明这一点。
- 嘿,我记得那门课。完全没想到。
- + 1。这是一个简单直观的解决方案,我已经用了很多次了。
- 这是一个很好的简单的解决方案,即使它不涵盖一般情况。这里有一个使用模的替代解决方案:((被除数-1)%除数+1
- 我喜欢它,当然,作为这种方法的副产品,分子中可能会有一些人为的溢出…
- @Pintag:这个主意不错,但是使用的模是错的。取13和3。预期结果5,但((13-1)%3)+1)给出结果1。采用正确的除法,1+(dividend - 1)/divisor给出了与正除数和正除数相同的结果。此外,无论是人为的,都不会出现溢流问题。
- 谢谢你的纠正,@lutzl。不知道我去年贴的时候在想什么:(
- 之所以出现这种情况,是因为在最近的一个问题stackoverflow.com/q/22254005/3088138中链接了这个答案,而我的评论是,通过您的评论改进了这个答案,这是表达所需计算的最优雅的方式,其他解决方案太复杂了。
- @卢兹尔:我很感谢你为我改进答案而给予我一些赞扬,但我承认我的"改进"是一个令人尴尬的失败。你在这件事上的慷慨和外交是值得赞赏的,因为那是维伦·丹克。
基于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表示延伸法
- 呃,什么?您的代码将被称为myint.divideBandoroundup(),并且将始终返回1,除非输入0会导致异常…
- @配置器-很好,我已经修复了代码!
- 史诗失败。(-2)。DivideByandroundup(2)返回0。
- 我参加聚会真晚了,但这段代码是编译的吗?我的数学类不包含接受两个参数的天花板方法。
你可以写一个助手。
1 2 3
| static int DivideRoundUp(int p1, int p2) {
return (int)Math.Ceiling((double)p1 / p2);
} |
- 不过,仍在进行相同数量的铸造。
- 大概这个操作足够聪明,可以把它放到一个函数中…
- @亡命之徒,随心所欲。但对我来说,如果他们不把它放进问题里,我一般认为他们没有考虑。
- 如果只是这样的话,写助手是没用的。相反,编写一个具有全面测试套件的助手。
- @你熟悉代码重用的概念吗?O.O
您可以使用如下内容。
1
| a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0) |
- 这是一个很好的方法,尽管使用(int)(float+0.5f)可能更容易理解。另外,它还使用除法、模、加法和if(或者类似的方法),所以速度非常慢。
- 但它只使用整数运算,而且与浮点运算相比,它们速度非常快。
- 但我还不满意-我相信有一个非常聪明的整数算术黑客…想一想…
- 好的一点-我也没有意识到除法在任何情况下都会被执行,整数除法比浮点除法快得多。
- 这段代码在两个方面显然是错误的。首先,语法上有一个小错误;您需要更多的括号。但更重要的是,它没有计算出期望的结果。例如,尝试使用a=-1000000和b=3999进行测试。正则整数除法的结果是-250。双师是-250.0625…理想的行为是围捕。显然,正确的从-250.0625取整到-250,但代码取整到-249。
- 你是对的。要修复括号和逻辑。我搞砸了逻辑——应该小于零,或者应该小于和。
- 我认为,把整个表达式颠倒起来会使它更容易理解。如果a*b>0和a%b!=0,加1。
- 代码仍然是错误的。试试它用+= 1000000和b= + 3999。整数除法是250,双除法是250.06,所以应该舍入到251。你的代码给出了250的结果。你为什么要繁殖?乘法使你毫无用处。如果你想做的是将"A"的符号与"B"的符号进行比较,那么肯定明智的做法是((a>0)=(b>0)),不是吗?
- 代码在理论上是正确的,但我错过了乘法溢出。我用乘法来避免另一个除法,因为如果a<0和b<0或a>0或b>0,需要进行调整。对于这个问题,a*b>0是最短的正确解。((a>0)==(b>0))在a==0和b==0时失败(至少)。
- (1)使用"a*b>0"不是最短的正确解,因为它不是正确的解。并且(2),如果a==0和b==0,那么您只是用0除以0,所以您已经抛出了一个异常。
- 很抱歉不得不一直这么说,但你的密码还是错了丹尼尔。1/2应该四舍五入为1,但您的代码将其四舍五入为0。每次我发现一个bug,你就通过引入另一个bug来"修复"它。我的建议是:不要这样做。当有人在你的代码中发现了一个bug,不要只是简单地把一个补丁拼凑起来,而不去想清楚是什么导致了这个bug。使用良好的工程实践,找出算法中的缺陷并加以修复。您的算法的三个错误版本都存在一个缺陷,那就是您不能正确地确定取整时间是"向下"的。
- 难以置信在这段代码中有多少个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具有相同的符号且不是同时为零,则必须进行调整。
- a*b>0之所以这样做,是因为a*b是正的,前提是a和b都是正的或都是负的。(2)设计引入的bug与您在删除的注释中发布的代码中出现的bug相同(result=a/b;if((result>0)&;&;a%b!=0))result++;)因为如果除数舍入为零,则无法正确检测符号。(c)我对((a>0)==(b>0))的评论是正确的。但我至少写过,事实上它在a=0和b<0时失败了。
- (0>0)==(-1>0)显然是正确的,但不应调整0/-1。实际上是第二个测试-A%B!=0-将评估为"假",然后将不进行任何调整,但它不能很好地表达该想法。
- 聪明到一半。可以理解吗?
- 这个答案可能是我写的最糟糕的东西,所以…现在它被埃里克的博客链接了…好吧,我的目的不是给出一个可读的解决方案;我真的锁定了一个简短而快速的黑客攻击。为了再次捍卫我的解决方案,我第一次得到了正确的想法,但没有考虑溢出。很明显,我错误的做法是在VisualStudio中发布代码而不编写和测试它。"修复"更糟糕——我没有意识到这是一个溢出问题,认为我犯了一个逻辑错误。结果,第一个"修复"没有改变任何东西;我只是颠倒了
- 逻辑和推送错误。在这里,我犯了下一个错误;正如埃里克已经提到的,我没有真正分析错误,只是做了第一件看起来正确的事情。我仍然没有使用VisualStudio。好吧,我当时很着急,没有花超过五分钟的时间来"修复",但这不应该成为一个借口。在我Eric反复指出错误之后,我启动了VisualStudio并发现了真正的问题。使用sign()进行的修复使这个东西更不可读,并将其转换为您真正不想维护的代码。我吸取了教训,再也不会低估自己有多狡猾了。
- 看似简单的事情可能会变成。
- 这不是很复杂和低效吗?
这里所有的解的问题是,要么它们需要一个强制转换,要么它们有一个数值问题。铸造浮动或双重总是一个选择,但我们可以做得更好。
当您使用@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也有一个舍入问题,但是这个结果会给你一个比这个站点上其他的更好的舍入解决方案。
- "当计算给我们一个大于或等于0.5的值时,我们需要找到一个方法来解决问题。"——你已经错过了这个问题的要点,或者总是圆起来,也就是说,OP想把0.001到1。