取反 ~ (NOT)

取反运算的符号是 ~,就是将 0 转为 1,将 1 转为 0 的操作,以 32 位整数 22 为例进行取反,首先将 22 转为二进制 00000000 00000000 00000000 00010110,再进行取反操作就是把 1 改为 0,0 改为 1,所以 ~22 的取反结果为 11111111 11111111 11111111 11101001

负数**

负数与正数属于有符号的的二进制,其区别在于高位是否为 1,负数的高位全部为 1,而正数的高位全部为 0,这里需要引入补码的概念,有符号的二进制是以补码形式存在,正数的补码与原码一致,而负数的补码,最高位是 1,取反后加 1 得到。

+22 的补码是 00000000 00000000 00000000 00010110
-22 的补码是将 +22 取反后得到 11111111 11111111 11111111 11101001 再加 1,最终结果为 11111111 11111111 11111111 11101010,所以,我们也可以得出公式 -22 = ~22 + 1,同时也得到 ~22 = -22 - 1 = -23

如果我们得到的是一个补码(通过最高位判断正负)如何计算他的十进制数值呢?

最高位为 0 表示正数,如补码 00000000 00000000 00000000 00010110 转为十进制就是 22
最高位为 1 表示负数,如补码 11111111 11111111 11111111 11101010,我们同样的需要取反后加 1 得到正数后加上负号即可。

  1. ~ 11111111 11111111 11111111 11101010
  2. -------------------------------------
  3. 00000000 00000000 00000000 00010101
  4. + 1
  5. -------------------------------------
  6. 00000000 00000000 00000000 00010110 (十进制 22)
  7. 22 再加上负号,最终结果为 -22

那么,如果我们要将一个正数转为负数的二进制又该怎么处理呢?同样的,可以通过取反再加一计算,过程如下。

~ 00000000 00000000 00000000 00010110
-------------------------------------
  11111111 11111111 11111111 11101001
+                                   1
-------------------------------------
  11111111 11111111 11111111 11101010 (十进制 -22)

与 & (AND)

运算的符号是 &,反映到二进制的表达式为:1&1 = 1 1&0 = 0&1 = 0&0 = 0
对 a = 1011(十进制 11),b = 0110(十进制 6)进行运算的过程如下。

a & b = 1011 & 0110

  1011
& 0110
------
  0010 (十进制 2)

或 | (OR)

运算的符号是 |, 反映到二进制的表达式为:1|1 = 1|0 = 0|1 = 1 0|0 = 0
对 a = 1011(十进制 11),b = 0110(十进制 6)进行运算的过程如下。

a | b = 1011 | 0110

  1011
| 0110
------
  1111 (十进制 15)

异或 ^ (XOR)

异或运算的符号是 ^, 反映到二进制的表达式为:1^1 = 0^0 = 0 1^0 = 0^1 = 1
对 a = 1011(十进制 11),b = 0110(十进制 6)进行异或运算的过程如下。

a ^ b = 1011 ^ 0110

  1011
^ 0110
------
  1101 (十进制 13)

左移 (<<)

左移表示将二进制向左边移动,并在最低位补 0,以 +11(二进制 1011)为例,+11 << 1 左移一位就变成 22(二进制 10110)。另外,左移一位等价于乘以 2,左移 n 位等价于乘以 位运算 | Bit Manipulation - 图1,当然这是基于不溢出的前提下。

再以负数 -11(二进制 11111111 11111111 11111111 11110101)为例,-11 << 1 左移一位后变成
11111111 11111111 11111111 11101010,通过取反加一后得到的是 -22,看来正负数的左移都可以表示乘以 位运算 | Bit Manipulation - 图2,当然还是基于不溢出的前提下。

右移 (>>)

右移表示将二进制向右边移动,准确来说是带符号右移,正数在最高位补 0,负数在最高位补 1,以 +11(二进制 1011)为例,+11 >> 1 右移一位就变成 +5(二进制 0101)。另外,根据左移乘 2 的经验,我们很容易联想到右移一位等价于除以 2,但只是后忽略了浮点数,左移 n 位等价于除以 位运算 | Bit Manipulation - 图3 但只是后忽略了浮点数。

刚才是 +11 作为例子右移一位,现在以 -11 (二进制 11111111 11111111 11111111 11110101) 为例,-11 >> 1 右移一位后变成 11111111 11111111 11111111 11111010,通过取反加一后得到的是 -6,所以右移除 2 对于负数不成立。

无符号右移 (>>>)

无符号右移与右移相似,但无符号右移的高位始终补 0,忽略正负数,以 -11(二进制 11111111 11111111 11111111 11110101)为例,无符号右移一位得到的是 2147483642(二进制 01111111 11111111 11111111 1111010),由于最高位变为 0,所以 -11 >>> 1 后变为正数。

实现基本运算

位运算实现加法

加法运算是利用 XOR 处理非进位相加,利用 AND 处理进位,以 +11 (1011) 和 +6 (0110) 为例,利用位运算实现加法的过程如下。

  1. 先通过 XOR 进行运算
  2. 再通过 AND 操作后左移一位实现进位效果
  3. 最后将 #1 和 #2 的结果再一次通过 XOR 运算,重复 #1 和 #2 的过程,直到 #2 的进位为 0 停止
  4. 完整过程如下 ```java 1011 (十进制 +11) ^ 0110 (十进制 +6)

1101 (记作 a)

1011 (十进制 +11)

& 0110 (十进制 +6)

0010

0010 << 1

0100 (记作 b)


```java

  1101 (a 值)
^ 0100 (b 值)
------
  1001 (记作 c)

  1101
& 0100
------
  0100

  0100 << 1
------
  1000 (记作 d)
  1001 (c 值)
^ 1000 (d 值)
------
  0001 (记作 e)

  1001
& 1000
------
  1000

  1000 << 1
------
 10000 (记作 f)
  00001 (e 值)
^ 10000 (f 值)
-------
  10001 (记作 g,十进制 17)

  00001
& 10000
-------
  00000 (进位为 0 停止,返回 g 值)

找到了规律后,再通过代码实现就变得简单了,实现如下,代码也非常精简。

public int add(int a, int b) {
  while (b != 0) {
    int carry = (a & b) << 1;
    a = a ^ b;
    b = carry;
  }
  return a;
}

对于上面这段代码,还可以更加精简,现在我们尝试利用已经运算好的 a = a ^ b 作为值计算出 carray(进位)数,假设 a = 1011(十进制 11),b = 0110(十进制 6)。

// a = 1011, b = 0110
a = a ^ b; // 1011 ^ 0110 = 1101

// a = 1101, b = 0110
b = a ^ b; // 1101 ^ 0110 = 1011 这个就是原来 a 的值

发现通过两次 XOR 运算之后结果还是原来 a 的值,相当于 a == (a ^ b) ^ b,因此可以作如下化简。

public int add(int a, int b) {
  while (b != 0) {
    a = a ^ b;
    b = ((a ^ b) & b) << 1;
  }
  return a;
}

其实还能更加简短,如下代码所示,但越是简短就越难维护和理解,因此还是推荐第一个写法。

public int add(int a, int b) {
  while ((b = (((a = a ^ b) ^ b) & b) << 1) != 0);
  return a;
}

如果明白了位运算实现加法,建议到 leetcode 自行实现一遍,对应 leetcode 第 371 题,虽然标识为 Easy 级别,但思考过程,我以为已经是 Hard 级别了,链接为:https://leetcode.com/problems/sum-of-two-integers/

还有一种是利用递归方式实现,代码如下,不需要其他技巧,实现上就非常简洁,不过需要用到栈,为了避免超出最大的栈深度,所以 Java 中的类库基本上都是使用迭代实现。

public int add(int a, int b) {
  return b == 0 ? a : add(a ^ b, (a & b) << 1);
}

位运算实现减法

有了加法基础,我们就可以利用到减法算术中,如 11 - 6 通过加法可转为 11 + (-6) 来表示,上面我们提到负数就是取反后再加一计算得到,所以 -6 可以通过 ~6 + 1 来表示。

public int add(int a, int b) {
  while (b != 0) {
    int carry = (a & b) << 1;
    a = a ^ b;
    b = carry;
  }
  return a;
}

public int subtract(int a, int b) {
  return add(a, add(~b, 1));
}

位运算实现乘法

通过学习减法是如何通过加法实现之后,对于乘法,我想你可能已经想到,m x n 就是 n 个 m 相加或 m 个 n 相加就可以得到了。

public int add(int a, int b) {
  while (b != 0) {
    int carry = (a & b) << 1;
    a = a ^ b;
    b = carry;
  }
  return a;
}

public int multiply(int a, int b) {
  int c = a <= 0 ? a : add(~a, 1); // 转为负正数处理
  int d = b <= 0 ? b : add(~b, 1); // 转为负正数处理
  int sum = 0;
  for (int i = 0; i < d && i <= 0; i = subtract(i, 1)) {
    sum = add(sum, c);
  }
  return (a ^ b) >= 0 ? -sum : sum; // 注意 sum 为负整数
}

注意参数 b 转为负整数处理了,这是因为负整数的绝对值要比真正数的绝对值要大,避免溢出问题,因此统一以负整数处理。虽然上面的代码可以运行,但效率实在有点太低了,如果参数 b 较大,则遍历的时间就会增加,有无更高效的方法呢?我们不妨参考十进制的乘法是怎么实现的?能否引用到二进制的乘法中呢?通过下面这个例子,我们进行简单的推理和验证。

  15         01111   a
x 22       x 10110   b
----    ----------
  30         00000
 30         01111
----       01111
 330      00000
         01111
        ----------
        101001010 (对应十进制 330)

01001010 计算后的十进制为 330,看来二进制的乘法也可以用十进制的乘法来运算,那么我们翻译成如下代码。

public int multiply(int a, int b) {
  if (a == 0)
    return 0;

  int sum = 0;
  while (b != 0) {
    if ((b & 1) == 1)
      sum = add(sum, a);
    a <<= 1;
    b >>>= 1;
  }
  return sum;
}

如果参数 b 的最低位为 1,则将参数 a 与 参数 b 相加,再将参数 a 左移一位,参数 b 右移一位,重复这个过程,直到参数 b 为零。

注意 a 或 b 等于 0 的情况,虽然迭代中已经可以处理 a 或 b 等于 0 的情况,但为了让程序更加有效率,我们可以提前处理。可能你也发现了 b >>>= 1 使用到了无符号右移,无符号右移可以使得参数 b 右移后高位补零,如果使用的是 b >>= 1 带符号右移,b 为负数时,则高位只会补一,从而导致循环无法退出。

位运算实现除法

除法与乘法相似,可对应减法,通过重复调用减法运算直到被除数为 0,但效率也是非常低下,代码如下,同样的为了避免溢出,统一以负整数处理。

public int add(int a, int b) {
  while (b != 0) {
    int carry = (a & b) << 1;
    a = a ^ b;
    b = carry;
  }
  return a;
}

public int subtract(int a, int b) {
  return add(a, add(~b, 1));
}

public int divide(int a, int b) {
  int c = a <= 0 ? a : add(~a, 1); // 转为负正数处理
  int d = b <= 0 ? b : add(~b, 1); // 转为负正数处理
  int quotient = 0;
  for (int i = c; i <= d && i <= 0; i = subtract(i, d)) {
    c = subtract(c, d);
    quotient = add(quotient, 1);
  }
  return (a ^ b) >= 0 ? quotient : -quotient;
}

与十进制除法一样,二进制也是可以通过竖式除法运算得到,相对于十进制,二进制其实更加简单,因为只有 0 或 1 两个选择,但十进制存在 0 到 9 十个选择。同样的,我们来验证一下,利用乘法运算的 15 x 22 = 330,通过二进制的 330 除以二进制的 15,观察是否能够得到二进制的 22

位运算 | Bit Manipulation - 图4

101001010 ÷ 01111 = 10110
      330 ÷ 15    = 22

通过上面这个例子看来也是可行的,再将其翻译成代码你会发现,由于负整数高位全为 1 的原因,这个竖式计算只能适应正整数,或者你会想通过将负整数转为正数再计算。貌似也是可以的,只不过 Integer.MIN_VALUE 要比 Integer.MAX_VALUE 的绝对值要大 1,也就是说将 Integer.MIN_VALUE 尝试转为正整数就会存在溢出的问题,|Integer.MIN_VALUE| = Integer.MIN_VALUE

换一个思路,不通过数学思维解决,我们可以通过算法思维重新思考问题,既然负整数转正整数,那我们全部都只操作负数,最后再将符号转回来即可。

算法思路

  1. 通过二分法,也就是分治的思想,将除数不断乘以 2,即不断左移一位,直到最后一个小于被除数的值
  2. 被除数减去 #1 部分的结果,再将剩余的数作为被除数重复 #1 和 #2 的步骤,直到被除数小于除数
  3. 最后将乘以 2 倍数相加即可得到结果

为了让这个过程更加清晰,下图以被除数是 167,除数是 3 为例的计算过程

image.png

将绿色部分的加粗数字相加则是最终结果,上图的最终结果为 位运算 | Bit Manipulation - 图6
而值得一提的是最后的 位运算 | Bit Manipulation - 图7 就是 位运算 | Bit Manipulation - 图8 取模的值。当然,这里仅能处理整数,如果加上浮点数就会更加复杂,这里就不展开了,有兴趣的读者可以自行实现。

public int divide(int dividend, int divisor) {
  // 处理溢出问题
  if (dividend == Integer.MIN_VALUE && divisor == -1)
    return Integer.MAX_VALUE;
  int end = dividend <= 0 ? dividend : add(~dividend, 1);
  int sor = divisor <= 0 ? divisor : add(~divisor, 1);
  int res = div(end, sor);
  return (dividend ^ divisor) >= 0 ? res : -res;
}

public int div(int dividend, int divisor) {
  if (dividend > divisor) return 0;
  int sum = divisor, count = 1;
  while (dividend <= (sum << 1) && (sum << 1) < 0) {
    sum <<= 1;
    count <<= 1;
  }
  return add(count, div(subtract(dividend, sum), divisor));
}

public int subtract(int a, int b) {
  return add(a, add(~b, 1));
}

public int add(int a, int b) {
  while (b != 0) {
    int carry = (a & b) << 1;
    a = a ^ b;
    b = carry;
  }
  return a;
}

循环的实现方式如下。

public int divide(int dividend, int divisor) {
  // 处理溢出问题
  if (dividend == Integer.MIN_VALUE && divisor == -1)
    return Integer.MAX_VALUE;
  int end = dividend <= 0 ? dividend : add(~dividend, 1);
  int sor = divisor <= 0 ? divisor : add(~divisor, 1);
  int quotient = 0;
  while (end <= sor) {
    int sum = sor;
    int count = 1;
    while ((sum << 1) >= end && (sum << 1) < 0) {
      sum <<= 1;
      count <<= 1;
    }
    end = subtract(end, sum);
    quotient = add(quotient, count);
  }
  return (dividend ^ divisor) >= 0 ? quotient : add(~quotient, 1);
}