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

我想确保整数的除法在必要时总是四舍五入。还有比这更好的办法吗?有很多演员都在选角。: -)

(int)Math.Ceiling((double)myInt1 / myInt2)
63494 次浏览

你可以使用下面这样的东西。

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

您可以编写一个helper。

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

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

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

最后一个基于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将为此工作。

经验教训

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

此外,虽然我的最终答案可能不像浮点数的答案那样“简单”或“明显”,甚至可能“快速”,但它对我来说有一个非常强大的救赎品质;我现在已经推理了答案,所以我实际上确定它是正确的(直到有更聪明的人告诉我- __abc0 -)。

为了对浮点答案有同样的确定性感觉,我必须做更多(可能更复杂)的思考,是否有任何条件下浮点精度可能会妨碍,以及Math.Ceiling是否可能在“刚刚正确”的输入上做一些不可取的事情。

走过的路

Replace(注意,我用myInt2替换了第二个myInt1,假设这就是你的意思):

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

:

(myInt1 - 1 + myInt2) / myInt2

唯一需要注意的是,如果myInt1 - 1 + myInt2溢出了你正在使用的整数类型,你可能得不到你想要的结果。

这是错误的: -1000000和3999应该是-250,这是-249

< p > 编辑: < br > 考虑到这与其他负myInt1值的整数解决方案具有相同的错误,可能更容易执行如下操作:

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

这应该只使用整数操作在div中给出正确的结果。

这是错误的: -1和-5应该是1,这是0

< p > 编辑(再一次,充满感情地): < br > 除法算符舍入到零;对于负结果,这是完全正确的,所以只有非负结果需要调整。还要考虑到DivRem无论如何只执行/%,让我们跳过调用(并从简单的比较开始,以避免在不需要时进行模计算):

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

这是错误的: -1和5应该是0,这是1

(在我为上次尝试辩护的时候,当我的大脑告诉我我已经晚睡了2个小时的时候,我就不应该尝试一个理性的答案)

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


获得正确的整数算术是困难的。正如迄今为止已经充分证明的那样,当你试图做一个“聪明”的把戏时,很可能你犯了一个错误。当发现一个缺陷时,改变代码来修复缺陷而不考虑修复是否会破坏其他东西并不是一个很好的解决问题的技巧。到目前为止,我想我们已经给出了5个不同的错误整数算术解来解决这个完全不是特别难的问题。

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

首先阅读您要替换的产品的规格说明。整数除法的规范清楚地说明:

  1. 除法使结果四舍五入为零

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

  3. 如果左操作数是最小的可表示整型,而右操作数是-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;
}

这聪明吗?不。漂亮吗?不。短吗?不。根据说明书正确吗?虽然看起来很不错。

我们是专业人士;使用良好的工程实践。研究你的工具,指定所需的行为,首先考虑错误情况,当你发现一个错误时,在你只是随机开始交换比较方向和破坏已经工作的东西之前,考虑你的算法是否有严重的缺陷。

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

在c#和Java中,对于正的被除数和除数,你只需要做:

( dividend + divisor - 1 ) / divisor

来源:数字转换,罗兰·巴恪思,2001年

这里所有解决方案的问题要么是它们需要强制转换,要么是它们有一个数值问题。转换为float或double总是一种选择,但我们可以做得更好。

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

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

有一个舍入误差。1 / 5会四舍五入,因为1% 5 != 0。但这是错误的,因为只有当用3替换1时才会发生舍入,所以结果是0.6。当计算给我们的值大于或等于0.5时,我们需要找到一种方法来四舍五入。上面例子中的模运算符的结果范围从0到myInt2-1。只有当余数大于除数的50%时,才会进行舍入运算。调整后的代码是这样的:

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

当然,我们在myInt2 / 2也有一个舍入问题,但是这个结果将为您提供一个比这个站点上其他结果更好的舍入解决方案。

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

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

对于有符号int型,这是不正确的

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

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。模也是一样的,只是加了除数。地板是一样的,但是在相反的条件下减去。

四舍五入,我想你的意思是总是从零开始。如果没有任何类型转换,则使用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;
}

如果roundup的意思是不管符号是什么,总是向上,那么

/// <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;
}