【问题标题】:What is the right way to find the average of two values?找到两个值的平均值的正确方法是什么?
【发布时间】:2014-09-15 05:29:33
【问题描述】:

我最近了解到整数溢出是 C 中未定义的行为(附带问题 - 它也是 C++ 中的 UB 吗?)

在 C 编程中,您通常需要找到 ab 两个值的平均值。然而,(a+b)/2 可能会导致溢出和未定义的行为。

所以我的问题是 - 在 C 中找到两个值 ab 的平均值的正确方法是什么?

【问题讨论】:

  • 展开表达式。是的,有符号整数溢出在 C 和 C++ 中都是 UB(虽然我不了解 C,但根据 C++ 标准,无符号算术不会溢出)。
  • a/2 + b/2 例如不会导致整数溢出。
  • 如果符号相反,(a + b)/2 不会溢出。如果标志相同,则a + (b - a) / 2 是安全的。
  • @DeepBlackDwarf: 但是 (1 + 3)/2 = 2 而 1/2 + 3/2 = 1。
  • @bodacydo:除非您想要 1 和 3 的平均值为 1 这样的结果,否则应避免使用 a/2 + b/2。你可能会做一些花哨的事情,比如a/2 + b/2 + (a%2 + b%2)/2,我认为你得到了正确的答案,但是......

标签: c undefined-behavior integer-overflow


【解决方案1】:

Secure Coding的帮助下

if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
    ((si_b < 0) && (si_a < (INT_MIN - si_b))))
{
  /* will overflow, so use difference method */
  return si_b + (si_a - si_b) / 2;
} 
else
{
 /* the addition will not overflow */
  return (si_a + si_b) / 2;
}

附录

感谢@chux 指出舍入问题。这是一个经过正确舍入测试的版本...

int avgnoov (int si_a, int si_b)
{
    if ((si_b > 0) && (si_a > (INT_MAX - si_b)))
    {
      /* will overflow, so use difference method */
      /* both si_a and si_b > 0; 
          we want difference also > 0
          so rounding works correctly */
      if (si_a >= si_b)
        return si_b + (si_a - si_b) / 2;
      else
        return si_a + (si_b - si_a) / 2;
    } 
    else if ((si_b < 0) && (si_a < (INT_MIN - si_b)))
    {
      /* will overflow, so use difference method */
      /* both si_a and si_b < 0; 
          we want difference also < 0
          so rounding works correctly */
      if (si_a <= si_b)
        return si_b + (si_a - si_b) / 2;
      else
        return si_a + (si_b - si_a) / 2;
    }
    else
    {
     /* the addition will not overflow */
      return (si_a + si_b) / 2;
    }
}

【讨论】:

  • 如果si_bINT_MIN 怎么办?然后在(si_a &lt; (INT_MIN - si_b)) 中出现整数溢出。
  • @ouah INT_MIN - INT_MIN 是溢出吗?我觉得更像0
  • @ouah 是的,但一元 - 不是二元 -。 6.5.6/6 "二进制- 运算符的结果是第一个操作数减去第二个操作数的差。"
  • +1,这是一个很好的解决方案,没有整数溢出,而且它还具有不使用% 运算符的优点。 % 是 c99 中的余数运算符,但在 c90 中它是余数或模运算符(对于负值,余数和模数不同)。
  • @chux,没问题,有替代品真好。
【解决方案2】:
(a >> 1) + (b >> 1) + (((a & 1) + (b & 1)) >> 1)

c int 数学中的移位语句 (x >> i) 相当于除以 2 的 i 次方。 所以语句 (a >> 1) + (b >> 1) 与 a/2 + b/2 相同。但是,还需要添加数字的截断部分的平均值。这个值可以通过掩码(a & 1)、加((a & 1) + (b & 1))和除(((a & 1) + (b & 1)) >> 1)得到。平均值变为 (a >> 1) + (b >> 1) + (((a & 1) + (b & 1)) >> 1)

注意:使用 >> 和 & 而不是 / 和 % 作为除法和余数运算符的原因之一是效率。

【讨论】:

  • "注意:使用 >> 和 & 而不是 / 和 % 作为除法和余数运算符的原因之一是效率。"你的编译器比这更聪明。请参阅Is multiplication and division using shift operators in C actually faster? 因此,为了代码的可读性,请使用 / 和 % 如果这是您想要的。
  • 即使在我知道所有优化都是自动完成的情况下,我也会尝试按照我想要执行的方式编写代码。
  • 我花了太长时间来编辑:我尝试编写我的代码,因为我希望它被执行,即使在我知道所有优化都是自动完成的情况下。 (虽然,可能存在比这更复杂的情况,例如:扩展环形缓冲区。)
  • 负值右移具有实现定义的结果。您最好使用除法,因为这保证会产生负结果或 0。我也不确定 a &amp; 1 是否保证等于 abs(a % 2)
  • 对于a = -2b = 1,此解决方案将得到-1 作为结果。但在 C (c99/c11) 中,(-2 + 1) / 20。该解决方案与 Vlad 的解决方案存在相同的问题; c99/c11 除法向0 截断,而不是向-inf 截断。
【解决方案3】:

一个简单的方法如下

int c = a / 2 + ( b + a % 2 ) / 2;

例如a和b可以表示为

a = 2 * n + r1;
b = 2 * m + r2;

然后

( a + b ) / 2 => ( 2 * n + r1 + 2 * m + r2 ) / 2 => 2 * n / 2 + ( b + r1 ) / 2

最后一个表达式给你

=> a / 2 + ( b + a % 2 ) / 2

更正确的表述如下

int c = a / 2 + b / 2 + ( a % 2 + b % 2 ) / 2;

例如,如果我们有

int a = INT_MAX;
int b = INT_MAX;

然后c计算为

int c = a / 2 + b / 2 + ( a % 2 + b % 2 ) / 2;

会给c == INT_MAX

编辑:在计算机操作员的效果和数学操作员的效果之间发现了有趣的差异。 比如根据数学-1可以表示为

-1 = -1 * 2 + 1 

根据公式

a = 2 * n + r1

2 * n 应为小于或等于 tp a 的整数

所以小于-1的数字是-2。 :)

我认为我展示的一般公式会起作用,它要求对于奇数负数,将被视为小于奇数负数的偶数负数。

似乎正确的公式看起来像

int c = ( a < 0 ? a & ~1 : a ) / 2 + 
        ( b < 0 ? b & ~1 : b ) / 2 + 
        ( ( a & 1 ) + ( b & 1 ) ) / 2;

请务必注意,从数学角度来看,-1-2 的平均值应等于 -2,并且公式给出了正确的结果。:)

【讨论】:

  • 另一个快速的问题 - 你是如何在最终表达式中得到模数 (a % 2) 的?这是从哪里来的?
  • @@bodacydo 当我将 a 表示为 2 * n + r1 时,r1 是 a % 2。r1 是 0 或 1。
  • 那么,如果a == b == INT_MAX 呢? INT_MAX % 2 通常不为零。 “简单的方法”仍然会产生溢出。
  • 正如我所说,“更正确的表达方式”不受这个问题的影响。但是,第一个表达式是:int c = a / 2 + ( b + a % 2 ) / 2; 受此问题影响。 -- 我不明白你为什么发布第一种方法。
  • 使用a = 2b = -1,您的方法给出1,但(2 - 1) / 20
【解决方案4】:

如果您担心溢出,可以将值转换为更大的类型以执行数学运算,然后进行边界检查。

【讨论】:

  • 从中获得乐趣。
  • 如果没有“更大的类型”呢?
  • @Thanatos 在这种情况下,您已经有无符号整数并且可以使用(a &gt;&gt; 1) + (b &gt;&gt; 1) + (((a &amp; 1) + (b &amp; 1)) &gt;&gt; 1)
  • 如果可用,最好同意转换为更宽的类型。然而,看不到“边界检查”的理由。
【解决方案5】:

这是来自Calculating the average of two integer numbers rounded towards zero in a single instruction cycle

(a >> 1) + (b >> 1) + (a & b & 0x1)

你必须考虑到:

  • 它的实现定义了右移一个负整数是否将零或一移到高位。许多 CPU 通常有两种不同的指令:算术右移(保留符号位)和逻辑右移(不保留符号位)。允许编译器选择其中一个(大多数编译器选择算术移位指令)。

    ISO/IEC 9899:2011 §6.5.7 位移位运算符

    ¶5 E1 >> E2 的结果是 E1 右移 E2 位位置。 [CUT] 如果 E1 有带符号类型和负值,则结果值是实现定义的。

    将表达式更改为:

    a / 2 + b / 2 + (a & b & 0x1)
    

    不是解决方案,因为逻辑右移相当于除以 2 only for positive or unsigned numbers 的幂。

  • (a &amp; b &amp; 0x1) 也没有很好的定义。当ab 都是奇数时,该项应该不为零。但它在补码表示和 ISO C 中失败,第 6.2.6.2/2 节指出an implementation can choose one of three different representations for integral data types

    • 二进制补码
    • 补码
    • 符号/大小

    (通常两者的补码远远超过其他)。

【讨论】:

  • 注意(a &gt;&gt; 1) + (b &gt;&gt; 1) + (a &amp; b &amp; 0x1)(a+b)/2 有大约1/4 的时间在典型的2 补码机器上使用各种int 的答案不同(1 倍)。 IOWs,答案有时会远离 0。第二种方法 a / 2 + b / 2 + (a &amp; b &amp; 0x1) 经常失败。例如-3, -3.
  • @chux 感谢您的观察,我已经更改了部分答案。
  • 虽然 ISO C 允许实现使用这些整数格式中的任何一种,但它还需要支持一种数据类型(至少 64 位的无符号长整型),这在任何无法高效的平台上都是不切实际的处理二进制补码数学,除非其本机整数类型为 65 位或更长。据我所知,从来没有,也永远不会有任何 C99 的生产实现或该语言的任何未来版本,它不使用补码数学。
  • @chux:我认为(a&gt;&gt;1)+(b&gt;&gt;1)+(a &amp; b &amp; 1) 将具有一致的向下舍入(而不是最小舍入)。以这种方式描述答案似乎比说它的行为在大约 1/4 的时间有所不同更有用。虽然某些应用程序可能需要最小的行为,但对于许多应用程序来说,更重要的是(不存在溢出)avg(x,y)+z == avg(x+z,y+z)。如果 avg(-1,0)==-1 且 avg(0,1)==0,则该等式将成立。但是,如果 avg(-1,0) 和 avg(0,1) 都为零,则不会。
【解决方案6】:

在整个[INT_MIN...INT_MAX] 范围内平均两个int 的最简单(通常也是最快)方法是使用更宽的整数类型。 (由@user3100381 建议。)让我们称之为int2x

int average_int(int a, int b) {
  return ((int2x) a + b)/2;
}

当然,这要求存在更宽的类型 - 所以让我们看看不需要更宽类型的解决方案。

挑战:

问:当int是奇数,而另一个是偶数时,应该以哪种方式取整?
A:按照上面的average_int() 向0 舍入。(截断)。

问:代码可以使用%吗?
A: 对于 C99 之前的代码,a % 2 的结果允许与a &lt; 0 不同的结果。所以我们不要使用%

问:int 是否需要正负数的大约对称范围?
答:自 C99 以来,负数的数量与正数的数量相同(或多 1 个)。让我们尽量不要这样。

解决方案:

执行测试以确定是否可能发生溢出。如果没有,只需使用(a + b) / 2。否则,将差值的一半(符号与答案相同)添加到较小的值。

下面给出了与average_int() 相同的答案,而无需求助于更广泛的整数类型。它 protects against int overflow 并且不需要 INT_MIN + INT_MAX 为 0 或 -1。它不依赖于编码为 2 的补码、1 的补码或符号大小。

int avgC2(int a, int b) {
  if (a >= 0) {
    if (b > (INT_MAX - a)) {
      // (a+b) > INT_MAX
      if (a >= b) {
        return (a - b) / 2 + b;
      } else {
        return (b - a) / 2 + a;
      }
    }
  } else {
    if (b < (INT_MIN - a)) {
      // (a+b) < INT_MIN
      if (a <= b) {
        return (a - b) / 2 + b;
      } else {
        return (b - a) / 2 + a;
      }
    }
  }
  return (a + b) / 2;
}

任何一对int最多出现3个if()s。

【讨论】:

    【解决方案7】:

    如果您只需要处理无符号整数类型(并且可以考虑二进制),您可以将您的加法分解为digitcarry。我们可以将a+b(无限精度)写成(a^b) + ((a&amp;b)&lt;&lt;1)),所以(a+b)/2 就是((a^b)&gt;&gt;1) + (a&amp;b)。最后一个表达式适合ab 的常见类型,因此您可以在代码中使用它:

    unsigned semisum(unsigned a, unsigned b)
    {
        return ((a^b)>>1) + (a&b);
    }
    

    【讨论】:

      【解决方案8】:

      如果只有 2 个元素可以避免溢出,最简单的答案是:

      (a/2) + (b/2) = average
      

      对于更多元素,您可以使用:

      (a/x) + (b/x) + (c/x) + (d/x) ..... = average //x = amount of elements
      

      数学角度,如果之前没有任何原始值发生过溢出,这将永远达到溢出,因为您不要真正将它们全部加在一起,而是先将它们分开,然后再将它们加在一起。因此,在计算期间执行的任何操作(包括结果)没有结果将永远大于(到 0 的任一侧)大于最大的初始元素(假设您只使用Real Numbers)。

      请执行以下操作:

      1. 确定“C”中元素的数量,我们称之为total
      2. 声明一个值来存储平均值,我们称之为average
      3. 声明一个值来存储余数,我们称之为remainder
      4. 遍历它们并:
        • 将当前元素除以总量,total
        • 将结果与平均值相加,average
        • 将除法后的余数相加,remainder
      5. 将余数除以平均值,average
      6. 按平均水平做您需要/打算做的事情。

      这将为您提供最多 1 的答案(十进制数字系统 [base 10])。我还不懂C++,所以只能举个C#的例子。

      C# 中的伪代码(仅供参考):

      int[] C = new int[20];            //The array of elements.
      int total = C.Length;             //The total amount of elements.
      int average = 0;                  //The variable to contain the result.
      int remainder = 0;                //The variable to contain all the smaller bits.
      foreach (int element in C)        //Iteration
      {
          int temp = (element / total); //Divide the current element by the total.
          average = average + temp;     //Add the result to the average.
          temp = (temp % total);        //Get the remainder (number that was not divided)
          remainder = remainder + temp; //Add remainder to the other remainders.
      }
      average = average + (remainder / total); // Adds the divided remainders to the average.
      

      压缩的 C# 示例:

      int[] C = new int[20];               //The array of elements.
      int total = C.Length;                //The total amount of elements.
      int average = 0;                     //The variable to contain the result.
      int remainder = 0;                   //The variable to contain all the smaller bits.
      foreach (int element in C)           //Iteration
      {
          average += (element / total);    //Add the current element divided by the total to the average.
          remainder += ( element % total); //Add the remainders together.
      }
      average += (remainder / total); //Adds the divided remainders to the total.
      

      【讨论】:

      • 但是(参见 Jonathan Leffler 的评论):(1 + 3)/2 = 21/2 + 3/2 = 1
      • 当所有n int 的值都为value%n == n-1 时,就会出现这种方法的弱点。在这种情况下,remainder 变为大约n*n。所以这个方法在n &gt; sqrt(INT_MAX) &gt; sqrt(32767)的时候有问题。为避免此限制,代码可能会使用 remainder = remainder + temp; average += remainder/total; remainder %= total. - 但当 n 很小时,这似乎很昂贵且不需要。
      猜你喜欢
      • 1970-01-01
      • 2017-09-28
      • 1970-01-01
      • 1970-01-01
      • 2014-05-26
      • 2022-06-30
      • 1970-01-01
      • 2013-04-25
      • 1970-01-01
      相关资源
      最近更新 更多