在C语言中取两个有符号数的平均值

14
假设我们有两个有符号整数 x 和 y,在 C 中,如何找到它们之间最精确的平均值?
我希望提供一种不依赖于任何机器/编译器/工具链特殊工作方式的解决方案。
我想到的最好的方法是:(a / 2) + (b / 2) + !!(a % 2) * !!(b %2) 是否有更准确、更快速、更简单的方法呢?
如果我们事先知道其中一个数比另一个数大怎么办?
谢谢。
D

检查编译器生成的汇编语言,因为编译器可能会针对常规的旧式求和除以二(带适当转换)计算进行优化。 - jonsca
如果编译器本身无法生成仅适用于特定平台的汇编代码,那将非常有用。想想有符号数甚至不是2补码的情况!考虑到这一点,编译器输出并不太有用... - McCormick
1
我有点喜欢以下代码:(a / 2) + (b / 2) + ((a % 2) + (b % 2)) / 2它的好处是它完全补充了模运算的定义,因此在数学意义上是完美准确的...但在C语言中是否准确呢? - McCormick
1
@jonsca:编译器可以忽略溢出,因为它是未定义的行为,因此不会给你一个解决方案。 - R.. GitHub STOP HELPING ICE
我认为a/2和b/2的四舍五入(以及a%2的符号)是机器特定的,如果它们是负数,这可能会影响适当的答案。标准提供的是:“如果商a/b是可表示的,则表达式(a/b)*b + a%b应等于a。”,所以它们至少是一致的,这意味着如果你使用基于数学的方法而没有任何!!技巧,它应该是正确的(但如果涉及到负数,它可能会向上或向下舍入)。 - Random832
1
@Random832:12年前它是与实现相关的。C99强制要求其表现与每个数学家的直觉相反,并朝着零舍入。 - R.. GitHub STOP HELPING ICE
7个回答

9

接受答案后(4年)

我希望函数int average_int(int a, int b)能够做到以下两点:
1. 对于所有的ab组合,能够在整个[INT_MIN..INT_MAX]范围内工作。
2. 与使用更宽的数学方法得出的(a+b)/2结果相同。

int2x存在时,@Santiago Alessandri的方法效果很好。

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

否则,可以参考@AProgrammer的变体:
注意:不需要更广泛的数学知识。
int avgC(int a, int b) {
  if ((a < 0) == (b < 0)) {  // a,b same sign
    return a/2 + b/2 + (a%2 + b%2)/2;
  }
  return (a+b)/2;
}

一个带有更多测试的solution|解决方案,但没有%

当没有溢出发生时,所有下面的解决方案都可以在1个单位内与(a+b)/2匹配,但我希望找到一个能够匹配所有int的解决方案。


@Santiago Alessandri的解决方案只适用于int的范围小于long long的范围的情况 - 这通常是成立的。

((long long)a + (long long)b) / 2

@AProgrammer被接受的答案,有大约四分之一的概率无法匹配(a+b)/2。例如,像a == 1,b == -2这样的输入。

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

@Guy Sirton,解决方案有大约1/8的失败率无法匹配(a+b)/2。例如,输入为a == 1,b == 0

int sgeq = ((a<0)==(b<0));
int avg = ((!sgeq)*(a+b)+sgeq*(b-a))/2 + sgeq*a;

@R..,解决方案有约四分之一的概率无法匹配(a+b)/2。例如,输入为a == 1, b == 1

return (a-(a|b)+b)/2+(a|b)/2;

@MatthewD,现在已删除的解决方案大约有5/6的时间无法匹配(a+b)/2。例如,输入如a == 1, b == -2

unsigned diff;
signed mean;
if (a > b) {
    diff = a - b;
    mean = b + (diff >> 1);
} else {
    diff = b - a;
    mean = a + (diff >> 1);
}

5
如果 (a^b)<=0,你可以放心地使用 (a+b)/2 而不用担心溢出。
否则,请尝试使用 (a-(a|b)+b)/2+(a|b)/2-(a|b) 的绝对值至少与 ab 中的较大者相等,并且符号相反,因此可以避免溢出。
我是凭记忆快速完成的,所以可能会有一些愚蠢的错误。请注意,这里没有任何机器特定的技巧。所有行为完全由 C 标准和它要求有符号值的二进制补码、反码或原码表示,并指定按位运算符按位表示工作所决定。不对,a|b 的相对大小取决于表示方法...
编辑:当它们具有相同的符号时,您也可以使用 a+(b-a)/2。请注意,这会偏向于 a。您可以将其翻转并偏向于 b。而我的解决方案则偏向于零,如果我没记错的话。
另一个尝试:一种标准方法是 (a&b)+(a^b)/2。在二进制补码中,它可以适用于任何符号,但我认为如果 ab 具有相同的符号,则它也适用于反码或原码。您可以检查一下吗?

我真的很喜欢这个解决方案,但似乎存在精度问题: - McCormick
哎呀,"a: -2147483647, b: -2147351867" 应该得到 -2147417757,但实际上得到了 -2147417756。 - McCormick
糟糕,当ab为负数时,在二进制补码中,a|b的幅度较小而不是较大。因此,舍入方向可能取决于有符号表示。 - R.. GitHub STOP HELPING ICE
虽然我认为你的答案同样正确,但我更喜欢另一个答案。不过还是谢谢你! - McCormick
没问题,我也更喜欢它。 - R.. GitHub STOP HELPING ICE

3

编辑:@chux - Reinstate Monica修复了版本问题:

if ((a < 0) == (b < 0)) {  // a,b same sign
  return a/2 + b/2 + (a%2 + b%2)/2;
} else {
  return (a+b)/2;
}

原始回答(如果未被接受,我会将其删除)。

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

看起来这是最简单的一个,不对实现特性做任何假设(它依赖于C99,将/的结果指定为“向0截断”,而对于C90,则取决于实现)。

它的优点是没有测试(因此没有昂贵的跳转),所有除法/余数都是2,因此编译器可以使用位操作技术。


2
哇,gcc为此生成的代码非常可怕。使用-Os时,它对所有内容都使用idiv,而使用-O2或更高版本时,则切换到位运算,但使用了大量的位运算...如果您有更大的类型可用,我认为您应该在更大的类型中进行平均值计算... - R.. GitHub STOP HELPING ICE
1
@R..,可怕的位运算处理是为了确保负值向0截断。 - AProgrammer
是的,但整个过程可以(而且应该)简化。我相信gcc只是使用其处理负除法的标准习语,并未在此后简化常见子表达式等内容。 - R.. GitHub STOP HELPING ICE
2
a = -6,b = 3,那么你的表达式得出的值是-2,但正确的值应该是-1。我在这里漏掉了什么? - zjk
5
这不是正确的答案,请查看@zjk的评论。我使用了一个SAT求解器进行验证。看起来它在负偶数和正奇数上失败了。 - Nishant

2

对于无符号整数,平均值是 (x+y)/2 的下取整。但对于有符号整数,这个公式不适用。对于两个和为奇数负数的整数,这个公式会失败,因为它们的下取整比平均值少1。

您可以在《Hacker's Delight》第2.5节中了解更多。

计算两个有符号整数的平均值而不溢出的代码如下:

int t = (a & b) + ((a ^ b) >> 1)
unsigned t_u = (unsigned)t
int avg = t + ( (t_u >> 31 ) & (a ^ b) )

我已经使用Z3 SMT求解器检查了它的正确性。


1

以下是一些可能有用的观察:

“最准确”的并不一定在整数中是唯一的。例如,对于1和4,2和3同样是“最准确”的答案。从数学上讲(而非C整数):

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

让我们尝试分解这个问题:

  • 如果 sign(a) 不等于 sign(b),那么 a+b 不会溢出。 这种情况可以通过比较二进制补码表示中的最高位来确定。
  • 如果 sign(a) 等于 sign(b),那么如果 a 大于 b,则 (a-b) 不会溢出。否则 (b-a) 也不会溢出。编辑:实际上两者都不会溢出。

您到底想要优化什么?不同的处理器架构可能有不同的最佳解决方案。例如,在您的代码中,将乘法替换为 AND 可能会提高性能。此外,在二进制补码架构中,您可以简单地使用 (a & b & 1)。

我只是随便丢一些代码,没有太仔细看,但也许有人可以使用并改进:

int sgeq = ((a<0)==(b<0));
int avg = ((!sgeq)*(a+b)+sgeq*(b-a))/2 + sgeq*a

所有三种有符号整数表示都包含一个“符号位”,您可以通过检查a^b是否小于零来测试ab是否具有相反的符号(因为如果两个数值都是负数,那么它们的符号位将在位表示中互相抵消)。 - R.. GitHub STOP HELPING ICE

-1
我会这样做,将两个数都转换为long long(64位有符号整数),将它们相加,这样不会溢出,然后将结果除以2:
((long long)a + (long long)b) / 2

如果你想要小数部分,将其存储为double类型。

需要注意的是,结果将适合于32位整数。

如果你正在使用最高级别的整数,则可以使用:

((double)a + (double)b) / 2

2
如果 ab 已经是最高级别的有符号整数类型,该怎么办? - R.. GitHub STOP HELPING ICE
请注意,在C语言中,当左操作数为负数时,>>的结果是由实现定义的。换句话说,取决于编译器和平台,它可能实际上会或不会进行符号扩展,并且甚至可能拒绝编译或崩溃程序(只要实现定义了此行为)。 - Anomie
不,它具有实现定义的,而不是行为。这是安全的,但该值可能会很奇怪。 :-) - R.. GitHub STOP HELPING ICE
3
任何明智的优化器都会将除以二优化为位移操作,当然在目标平台上不支持时也不会这样做。因此最好保留它作为除法运算,这样更易于阅读。 - SoapBox
4
将64位整数转换为双精度浮点数会比使用 a/2 + b/2 更丢失精度。 - AProgrammer
显示剩余2条评论

-2

这个答案适用于任何数量的整数:

    int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    decimal avg = 0;
    for (int i = 0; i < array.Length; i++){
        avg = (array[i] - avg) / (i+1) + avg;
    }

这个测试期望平均值为5.0


1
这似乎不是C语言。 - Toby Speight

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接