使用按位运算符和布尔逻辑的绝对值 abs(x)

bet*_*eta 52 c c++ bit-manipulation absolute-value language-lawyer

这是如何运作的?

这个想法是abs(x)对整数使用按位运算符(假设为 32 位字):

y = x >> 31
(x + y) ^ y // This gives abs(x) (is ^ XOR)?
Run Code Online (Sandbox Code Playgroud)

Eri*_*hil 58

假设 32 位字,如问题所述:

对于 negative xx >> 31在 C 和 C++ 标准中是实现定义的。代码的作者期望二进制补码整数和算术右移,x >> 31如果符号位为零,则生成全零位,如果符号位为一,x则生成全一位。

因此,如果x是正数或零,y则为零,并且x + yx,因此(x + y) ^ yx,这是 的绝对值x

如果x为负,y则为全1,表示补码中的?1。然后x + yx - 1。然后与所有 1 进行异或将所有位反转。将所有位取反就相当于取二的补码减一,二的补码是用来取反二进制补码格式的整数的方法。换句话说,q与所有的异或给出-q - 1. 所以x - 1全部为一异或产生-(x - 1) - 1= -x + 1 - 1= -x,这是的绝对值x除时x为格式的最小可能值(?2147483648用于32位2的补码),在这种情况下,绝对值(2147483648)是太大而不能表示,并且生成的位模式只是原始的x.

  • @beta *IF* 您的 C 或 C++ 实现遵循二进制补码整数和算术右移。在 C++20 之前,仅需要整数来表示 `-(2^(N-1) - 1)` 到 `+(2^(N-1) - 1)` 之间的所有值或补码。大多数编译器都执行二进制补码,因为它们不是怪物,但这是可能的。C++20 标准要求整数为二进制补码。正如埃里克上面所说,负整数的右移是实现定义的,但*可能*算术右移。 (3认同)
  • 实际上,Y的类型没有指定,你错过了这个算法的一个非常重要的部分,那就是“如果Y是32位无符号,这仍然有效”。这是因为它使用了创造性的位映射,利用了 2 的补码数字的定义,其工作原理与系统的符号无关。这是一个古老的数字食谱技巧,它实际上(当时)改进了可移植性的某些方面,以我们现在希望没有人以这种方式“改进”的方式。 (3认同)
  • @EdwinBuck:是的,现在优化编译器知道这个技巧,并且如果你写“unsigned absx = x<0?”,就会在适当的时候使用它。-x : x;` (或者更好的方法,可以避免“-x”中最负 2 的补数的有符号溢出 UB)。 (3认同)

Ayx*_*xan 18

这种方法依赖于许多特定于实现的行为:

  1. 它假设x是 32 位宽。虽然,你可以解决这个问题x >> (sizeof(x) * CHAR_BIT - 1)
  2. 它假设机器使用二进制补码表示。
  3. 右移运算符从左到右复制符号位。

3 位示例:

101 -> x = -3
111 -> x >> 2

101 + 111 = 100 -> x + y

100 XOR 111 -> 011 -> 3
Run Code Online (Sandbox Code Playgroud)

这不是便携式的。

  • 在OP问题中,“xor”运算是使用“y”(而不是“x”) (3认同)
  • 该代码不假设“int”是 32 位,因为问题中根本没有“int”。它明确指出它正在处理的字,无论它们是什么类型,都是 32 位的。 (3认同)
  • 我不是其中之一,但这并不能回答OP的问题。只有评论才可以 (2认同)

Edw*_*uck 12

这不是便携式的,但我会解释为什么它仍然有效。

第一个操作利用了 2 的负数补码的特性,即第一位如果为负则为 1,如果为正则为 0。这是因为数字范围从

下面的示例针对 8 位,但可以外推到任意数量的位。在您的情况下,它是 32 位(但 8 位更容易显示范围)

10000000 (smallest negative number)
10000001 (next to smallest)
...
11111111 (negative one)
00000000 (zero)
00000001 (one)
...
01111110 (next to largest)
01111111 (largest)
Run Code Online (Sandbox Code Playgroud)

使用数字的 2 的补码编码的原因来自这样的特性:将任何负数添加到它的正数会产生零。

现在,要创建 2 的补数的负数,您需要

  1. 取输入数字的倒数(按位非)。
  2. 添加一个。

将 1 添加到其中的原因是强制加法功能将寄存器清零。你看,如果它只是 x + ~(x),那么你会得到一个全 1 的寄存器。通过给它加一个,你会得到一个级联进位,它产生一个零寄存器(寄存器的进位输出为 1)。

这种理解对于了解您提供的算法(大部分)有效的“原因”很重要。

y = x >> 31   // this line acts like an "if" statement.
              // Depending on if y is 32 signed or unsigned, when x is negative, 
              // it will fill y with 0xFFFFFFFF or 1.  The rest of the 
              // algorithm doesn't, care because it accommodates both inputs.
              // when x is positive, the result is zero.
Run Code Online (Sandbox Code Playgroud)

我们将探索(首先x为正)

(x + y) ^ y   // for positive x, first we substitute the y = 0
(x + 0) ^ 0   // reduce the addition
(x) ^ 0       // remove the parenthesis
x ^ 0         // which, by definition of xor, can only yield x
x
Run Code Online (Sandbox Code Playgroud)

现在让我们探索一下(x 为负数,y 为 0xFFFFFFFF(y 有符号))

(x + y) ^ y   // first substitute the Y
(x + 0xFFFFFFFF) ^ 0xFFFFFFFF // note that 0xFFFFF is the same as 2's complement -1
(x - 1) ^ 0xFFFFFFFF // add in a new variable Z to hold the result
(x - 1) ^ 0xFFFFFFFF = Z  // take the ^ 0xFFFFFFFF of both sides
(x - 1) ^ 0xFFFFFFFF ^ 0xFFFFFFFF = Z ^ 0xFFFFFFFF // reduce the left side
(x - 1) = z ^ 0xFFFFFFFF // note that not is equivalent to ^ 0xFFFFFFFF
(x - 1) = ~(z) // add one to both sides
x - 1 + 1 = ~(z) + 1 //  reduce
x = ~(z) + 1  // by definition z is negative x (for 2's complement numbers)
Run Code Online (Sandbox Code Playgroud)

现在让我们探索一下(x 是负数,y 是 0x01(y 是无符号的))

(x + y) ^ y   // first substitute the Y
(x + 1) ^ 0x00000001 // note that x is a 2's complement negative, but is
                     // being treated as unsigned, so to make the unsigned
                     // context of x tracable, I'll add a -(x) around the X
(-(x) + 1) ^ 0x00000001 // which simplifies to
(-(x - 1)) ^ 0x00000001 // negative of a negative is positive
(-(x - 1)) ^ -(-(0x00000001)) // substituting 1 for bits of -1
(-(x - 1)) ^ -(0xFFFFFFFF) // pulling out the negative sign
-((x-1) ^ 0xFFFFFFFF) // recalling that while we added signs and negations to
                      // make the math sensible, there's actually no place to
                      // store them in an unsigned storage system, so dropping
                      // them is acceptable
x-1 ^ 0XFFFFFFFF = Z // introducing a new variable Z, take the ^ 0xFFFFFFF of both sides
x-1 ^ 0xFFFFFFFF ^ 0xFFFFFFFF = Z ^ 0xFFFFFFFF // reduce the left side
x-1 = z ^ 0xFFFFFFFF // note that not is equivalent to ^ 0xFFFFFFFF
x-1 = ~(z) // add one to both sides
x - 1 + 1 = ~(z) + 1 //  reduce
x = ~(z) + 1  // by definition z is negative x (for 2's complement numbers, even though we used only non-2's complement types)
Run Code Online (Sandbox Code Playgroud)

请注意,虽然上述证明对于一般解释是可以通过的,但实际情况是这些证明不包括重要的边缘情况,例如 x = 0x80000000 ,它表示绝对值大于任何正 X 的负数,它可以存储在相同的位数。