四舍五入整数除法(而不是截断)


76

我很想知道如何将数字四舍五入到最接近的整数。例如,如果我有:

如果以浮点数计算,则为14.75;如何将结果存储为“ a”中的15?


1
请说明:最接近的十分之一(14.8)或最接近的整数(15)?
卡尔·斯莫特里奇

2
@卡尔:由于a是一个整数,因此必须四舍五入到最接近的整数,不是吗?
乔纳森·勒夫勒

3
@Jonathan:那样看,但是为什么在问题陈述中说“最接近的十分之一”呢?语句不正确,或者OP想要做的事情他没有明确指定。这就是要求澄清的想法。
卡尔·斯莫特里奇

@卡尔:嗯,是的,很好!我不确定为什么将结果存储为整数时为什么要问十分之一。
乔纳森·勒夫勒

除了我的C宏和gcc语句表达式版本外,我还刚刚添加了该版本的C ++函数模板版本,并进行类型检查以确保仅使用整数类型,以防万一有人需要使用包含宏的硬核C ++程序。皱着眉头:: stackoverflow.com/questions/2422712/…。(以及ASCII皱眉的来源:(͡°͜ʖ͡°)
加布里埃尔·斯台普斯

Answers:


49

这仅在分配给int时有效,因为它会丢弃'。'之后的任何内容。

编辑: 此解决方案将仅在最简单的情况下工作。一个更强大的解决方案是:


23
请注意,这是浮动指针解决方案。我建议您使用整数运算有很多原因。
Yousf 2010年

11
因为在没有FPU的系统上,这确实造成了非常糟糕的代码。
Michael Dorgan

7
那就是问题所在。OP的问题完全可以解决,而无需使用任何浮点数,因此将不依赖于FPU支持的存在或良好。而且,在大多数架构上(包括那些具有出色FPU支持的架构),速度更快(如果需要计算很多)。还要注意,对于浮点数不能准确表示给定整数值的较大数字,您的解决方案可能会出现问题。
肖恩·米德迪奇

8
-1,当sizeof(int)> = sizeof(float)时,这给许多值一个错误的答案。例如,一个32位浮点数使用一些位来表示指数,因此它不能精确地表示每个32位int。所以12584491/3 = 4194830.333 ...,应该四舍五入为4194830,但是,在我的机器上不能完全以浮点数形式表示12584491,上述公式四舍五入为4194831,这是错误的。使用double更安全。
Adrian McCarthy

1
真正的问题是,为什么要将这样的值表示为整数类型。double保留最高达2 ^ 53的整数,并允许您轻松指定错误的取整方式。
马尔科姆·麦克莱恩

136

整数舍入的标准习惯用法是:

您将除数减一加到除数中。


8
如果要进行数学回合(14.75至15、14.25至14)怎么办?
克里斯·卢茨

11
gh ...那么您必须考虑...或多或少加(n-1)/ 2。对于n == 4,您希望x%n∈{0,1}向下取整,而x%n∈{2,3}向上取整。因此,您需要添加2,即n /2。对于n == 5,您希望x%n∈{0,1,2}向下舍入,而x%n∈{3,4}向上舍入。 ,因此您需要再次添加2 ...因此:int i = (x + (n / 2)) / n;
乔纳森·勒夫勒

7
这种方法对肯定有效int。但是,如果除数或除数为负,则会产生错误的答案。@caf的提示也不起作用。
恢复莫妮卡

8
(原始)标题和问题要求两个不同的内容。标题说的是四舍五入(这是您回答的内容),但正文说的是四舍五入到最接近的位置(这是接受的答案尝试的内容)。
Adrian McCarthy

5
请注意,由于整数溢出而c = (INT_MAX + (4 - 1)) / 4;给出的c = -536870911...
KrisWebDev '16

56

适用于任何除数和除数符号的代码:

在回应“为什么这实际上起作用?”的评论时,我们可以将其分开。首先,请注意这n/d将是商,但会将其截断为零而不是四舍五入。如果在除法之前将分母的一半加到分子上,但是仅当分子和分母具有相同的符号时,才能得到四舍五入的结果。如果符号不同,则必须在分母之前减去分母的一半。将所有内容放在一起:

如果您更喜欢宏:

linux内核宏DIV_ROUND_CLOSEST对负除数不起作用!

编辑:这将工作而不会溢出:


8
除了int接近min / max int的值之外,这是迄今为止最好的解决方案。
chux-恢复莫妮卡

1
为什么这实际上起作用?这背后的数学概念是什么?
Tobias Marschall,

@TobiasMarschall这等效于floor(n / d + 0.5),其中n和d是浮点数。
vll

22

您应该改用如下方式:

我认为您确实是在尝试做一些更一般的事情:

x +(y-1)可能会溢出,给出错误的结果;而x-1仅在x = min_int时才下溢。


1
61.0 / 30.0 = 2.03333(3)。因此,取整应该为2,但(61-1)/ 30 + 1 = 3
nad2000

3
@ nad2000为什么2.0333 ..四舍五入最多为2?
Gurgeh,2012年

8
如果x = 0,则此方法不起作用。如果x = 0为0,则x / y的预期结果四舍五入。然而,该解决方案的结果为1。其他解决方案给出了正确的答案。
大卫

实际上,这个答案根本不正确。它适用于一些数字,但失败了很多。稍后在线程中查看我更好的(我希望)答案。
WayneJ,2013年

如果答案是正确的,为什么不删除它呢?
再见SE

13

(编辑)使用浮点数舍入整数是解决此问题的最简单方法;但是,根据问题集可能是可行的。例如,在嵌入式系统中,浮点解决方案可能会过于昂贵。

使用整数数学执行此操作比较困难,有点不直观。第一个发布的解决方案可以很好地解决我曾经使用过的问题,但是在对整数范围内的结果进行特征化后,结果通常很糟糕。翻阅几本有关位旋转和嵌入式数学的书,收效甚微。几个注意事项。首先,我只测试了正整数,我的工作不涉及负分子或分母。其次,对32位整数进行详尽的测试在计算上是令人望而却步的,因此我从8位整数开始,然后确保获得16位整数的相似结果。

我从之前提出的两种解决方案开始:

#define DIVIDE_WITH_ROUND(N, D) (((N) == 0) ? 0:(((N * 10)/D) + 5)/10)

#define DIVIDE_WITH_ROUND(N, D) (N == 0) ? 0:(N - D/2)/D + 1;

我的想法是,第一个版本将大量溢出,而第二个版本将少量溢出。我没有考虑两件事。1.)第二个问题实际上是递归的,因为要获得正确的答案,您必须正确舍入D / 2。2)在第一种情况下,您经常溢出然后下溢,两者相互抵消。这是两种(不正确)算法的误差图:Divide with Round1 8 bit x=numerator y=denominator

该图表明,第一种算法仅对小分母(0 <d <10)不正确。没想到它实际上处理numerators比第二版更好。

这是第二种算法的图解: 8 bit signed numbers 2nd algorithm.

正如预期的那样,它对于小型分子失败,但对于比第一版​​本更大的分子失败。

显然,这是正确版本的更好起点:

#define DIVIDE_WITH_ROUND(N, D) (((N) == 0) ? 0:(((N * 10)/D) + 5)/10)

如果您的分母> 10,那么它将正常工作。

D == 1需要特殊情况,仅返回N。D == 2需要特殊情况,= N / 2 +(N&1)//如果为奇数则四舍五入。

一旦N足够大,D> = 3也有问题。事实证明,较大的分母只有较大的分子才有问题。对于8位带符号的数字,问题点是

if (D == 3) && (N > 75))
else if ((D == 4) && (N > 100))
else if ((D == 5) && (N > 125))
else if ((D == 6) && (N > 150))
else if ((D == 7) && (N > 175))
else if ((D == 8) && (N > 200))
else if ((D == 9) && (N > 225))
else if ((D == 10) && (N > 250))

(返回D / N)

因此,一般而言,特定分子变质的尖端就在某处
N > (MAX_INT - 5) * D/10

这并不确切,但很接近。如果使用C位除法(截断),则使用16位或更大的数字时,错误<1%。

对于16位带符号的数字,测试将是

if ((D == 3) && (N >= 9829))
else if ((D == 4) && (N >= 13106))
else if ((D == 5) && (N >= 16382))
else if ((D == 6) && (N >= 19658))
else if ((D == 7) && (N >= 22935))
else if ((D == 8) && (N >= 26211))
else if ((D == 9) && (N >= 29487))
else if ((D == 10) && (N >= 32763))

当然,对于无符号整数,MAX_INT将替换为MAX_UINT。我敢肯定,有一个确切的公式可以确定适用于特定D和位数的最大N,但是我没有更多的时间来解决这个问题了……

(此刻我似乎缺少此图,我将在以后进行编辑和添加。)这是8位版本的图,上面带有特殊情况:![[8位带特殊情况的签名0 < N <= 10 3

请注意,对于图形中的所有错误,对于8位,错误为10%或更少,而16位<0.1%。


1
你是对的。第一个宏不正确(我很难找到这个方法。)事实证明,这比我预期的要难。我更新了该帖子,承认不正确,并进行了一些进一步的讨论。
韦恩·J

7

如所写,您正在执行整数运算,该运算将自动截断任何十进制结果。要执行浮点运算,请将常量更改为浮点值:

或将它们强制转换为afloat或其他浮点类型:

无论哪种方式,都需要对标头中的round()函数进行最后的四舍五入math.h,因此请确保#include <math.h>并使用与C99兼容的编译器。


典型float(IEEE)将此解决方案的有用范围限制为abs(a / b)<16,777,216。
chux-恢复莫妮卡


5

从Linux内核(GPLv2):


typeof()C或编译器特定扩展的一部分吗?
chux-恢复莫妮卡

4
@chux:这是GCC扩展名。这不是标准C的一部分
玉米秆

检查宏中有符号和无符号args的一种好方法,因此无符号args可以完全省去分支和额外的指令。
彼得·科德斯

4

另一个有用的宏(必须具备):


1
你的括号让我头晕。
安德鲁

10
当然,这看起来像是LISP的坏情况,但是省略每个参数周围的括号然后评估ABS(4&-1)会更糟。
贾斯汀

3
他想要的ROUND不是CEIL
phuclv

4

通过检查是否存在余数,可以手动舍入整数除法的商。


1
当除数不是2时,这种取整不是很频繁吗?
Doug McClean

这总是像ceil函数一样四舍五入,而不是适当的舍入
phuclv

3

这是我的解决方案。我喜欢它是因为我发现它更具可读性,并且因为它没有分支(ifs和ternaries)。

完整的测试程序,说明预期的行为:


&((a ^ b) & 0x80000000) >> 31;是多余的,因为低位将移位后扔掉无论如何
phuclv

3

我喜欢从@ericbn借来定义


2

TLDR:这是一个宏。用它!

用法示例:

完整答案:

其中一些答案看起来很疯狂!Codeface钉了它!(请参见@ 0xC0DEFACE的答案)。我真的很喜欢无形式的宏或gcc语句表达式形式,而不是函数形式,所以,我写了这个答案,并详细说明了我在做什么(即:为什么这样做在数学上可行)并将其分为2种形式:

1.宏形式,带有详细的注释来解释整个事情:

2. GCC声明表达形式:

在这里GCC声明表情多了几分

3. C ++函数模板形式:

(新增2020年3月/ 4日)

在此处运行并测试其中一些代码:

  1. OnlineGDB:除法期间的整数舍入

相关答案:

  1. C编程中的定点算术-在此答案中,我研究了如何将整数舍入到最接近的整数,然后是第十位(小数点右边的1个小数位),第一百位(2个小数位),千位( 3个十进制数字),等等。在我的代码注释中搜索该部分的答案,称为BASE 2 CONCEPT:获取更多详细信息!
  2. 我对gcc语句表达式的一个相关答案:C中的MIN和MAX
  3. 具有固定类型的函数形式:舍入整数除法(而不是截断)
  4. 整数除法的行为是什么?
  5. 要舍入而不是舍入到最接近的整数,请遵循以下类似模式:舍入整数除法(而不是截断)

参考文献:

  1. https://www.tutorialspoint.com/cplusplus/cpp_templates.htm

待办事项:测试是否为负输入并更新此答案(如果可行):


绝对TLDR
chqrlie

复制代码块。删除所有评论。3种方法中的每一种都像1到4行。但是,真正发生的事情及其工作方式的详细信息值得作为教学平台进行详细的解释。
加布里埃尔·斯台普斯

1
简短的答案有缺陷:ROUND_DIVIDE(-3 , 4)计算结果为0,这不是最接近的整数。冗长的解释根本没有解决这个问题。(int)round(-3.0 / 4.0)将评估为-1
chqrlie

1
如果我说错了,那我去修理时会找出来的。太晚了。我还将在测试用例中添加负数。
加布里埃尔·斯台普斯

1
我没有忘记这一点。我的直觉是正确的,在某些情况下需要将加法交换为减法,这需要基于每个输入的负数进行逻辑XOR。我正在首先完全在本地计算机上重写此答案,将其修复为处理负数,还添加了宏以进行四舍五入。完成后,我将拥有,,和之类的东西DIVIDE_ROUNDUP(),它们都将处理正整数和负整数输入。希望我能赢得您的支持。我当然会自己使用这些。DIVIDE_ROUNDDOWN()DIVIDE_ROUNDNEAREST()
加布里埃尔·斯台普斯

1

例如59/4商= 14,tempY = 2,余数= 3,余数> = tempY,因此商= 15;


1
PS。“因此”和“因此”比“因此”听起来更崇高。
luser droog 2011年

对于负数,这不能正常工作-请考虑divide(-59, 4)
caf

1
  1. 让精确的浮点值59.0 / 4为x(此处为14.750000)
  2. 令小于x的最小整数为y(此处为14)
  3. 如果xy <0.5,则y为解
  4. 否则y + 1是解决方案

请写一些解释,这对每个人来说都不是小事。
mraron

现在看起来好点了吗?
西瓦达特拉

这是最糟糕的解决方案。进行2次慢速分割不是人们想要的。可以通过截断a而不是进行另一除法来获得b
phuclv


0

如果要除以正整数,则可以将其向上移位,进行除法,然后检查实b0右侧的位。换句话说,100/8是12.5,但将返回12。如果您执行(100 << 1)/ 8,则可以检查b0,然后在将结果向下移后将其向上舍入。


0

对于某些算法,当“最近”为平局时,您需要一个一致的偏差。

无论分子或分母的符号如何,此方法均有效。


如果要匹配round(N/(double)D)(浮点除法和舍入)的结果,则可以使用以下几种变体来产生相同的结果:

注意:(abs(d)>>1)vs .的相对速度(d/2)可能取决于平台。


正如@caf在对另一个答案的评论中所指出的那样,使用四舍五入方法存在溢出风险(因为它会在除法之前修改分子),因此如果您推动的范围限制,则此功能不合适int
布伦特·布拉德本

1
顺便说一句,如果您正好被2的幂除(这也意味着一个正数的除数),则可以利用以下事实:带符号的右移移位具有向右舍入的无穷大除法(不同于除法运算符,该运算符会舍入为零)以避免使用任何条件逻辑。因此,公式变为return (n+(1<<shift>>1))>>shift;,这简化了对形式(n+C)>>shift(其中,C=(1<<shift>>1)如果shift刚好是一个常数。
布伦特布拉德

“中间值偏差”问题仅与整数之间精确地为0.5的情况有关,因此可以肯定地说。例如,对于某种图形表示,您可能希望看到零附近的连续性而不是对称性。
布伦特·布拉德本

0

对于没有浮点数或条件分支的正数和负数操作数,以下内容正确地将商数舍入到最接近的整数(请参见下面的程序集输出)。假设N位2的补码整数。

ROUNDING的值与除数(x)具有相同的符号,而除数的大小(y)具有一半。因此,在整数除法截断所得商之前,将ROUNDING添加到被除数会增加其幅度。这是针对32位ARM Cortex-M4处理器进行-O3优化的gcc编译器的输出:


0

除以4的一些替代方法

或一般来说,除以2的任意幂

如果小数部分⩾0.5,即第一位数字⩾base / 2,则四舍五入。在二进制中,这等同于将第一个小数位添加到结果中

该方法在带有标志寄存器的体系结构中具有优势,因为进位标志将包含移出最后一位。例如在x86上,可以将其优化为

它也很容易扩展以支持有符号整数。请注意,负数的表达式为

我们可以使它同时适用于正值和负值


0

如先前贡献者所述,基本的舍入除法算法是在除法之前将分母的一半加到分子上。当输入是无符号的时,这很简单,而当涉及带符号的值时,情况就并非如此。以下是一些解决方案,这些解决方案可以通过GCC为ARM(thumb-2)生成最佳代码。

已签名/未签名

第一行代码通过整个字复制分子符号位,创建零(正)或-1(负)。在第二行中,此值(如果为负)用于使用2的补码负号取反:取整和增量。先前的答案使用条件语句或相乘来实现。

签名/签名

我发现我得到了带有条件表达式的最短代码,但前提是我通过计算舍入值d / 2帮助了编译器。使用2的补码求反是很接近的:

除以2的幂

整数除法向零截断,而移位向负无穷大。这使舍入移位变得更加简单,因为无论分子的符号是什么,您总是添加舍入值。

表达式是相同的(根据类型生成不同的代码),因此宏或重载函数都可以同时使用。

传统方法(四舍五入除法的工作方式)是将除数的一半加1 <<(s-1)。取而代之的是,我们少移位一个,再加一个,然后进行最后的移位。这样可以节省创建一个非平凡的值(即使是常量),也可以省去将其放入机器的寄存器。


int sgn = n >> (sizeof(n)*8-1); // 0 or -1:否,该行为未由C标准定义。您应该使用int sgn = ~(n < 0);
chqrlie

我关心的是ARM微控制器的速度和尺寸。该表达式又~(n < 0)生成一条指令。此外,原始表达式适用于任何使用8位字节和二进制补码的体系结构,我认为它可以描述所有现代机器。
DosMan

-1

我遇到了同样的困难。下面的代码应适用于正整数。

我尚未编译它,但我在Google电子表格(我知道,wtf)上测试了该算法,并且该算法正在运行。


看不到需要if(denominator == 1) return numerator;。目的是什么?
chux-恢复莫妮卡

-1

更安全的C代码(除非您有其他处理/ 0的方法):

return (_divisor > 0) ? ((_dividend + (_divisor - 1)) / _divisor) : _dividend;

当然,这不能解决由于无效输入数据而导致返回值不正确而引起的问题。

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.