计算机的世界,不是0就是1,我是指bit。bit作为信息量最基础的的度量单位,也是我们计算机中的基础数据结构。我们在进行应用开发时,常常忘记了梦的开始的地方其实是0和1,而位运算,即对bit位数据进行一元或二元操作的运算,我们做算法题时会发现,合理运用位运算,能大幅提高运算效率。位运算的题还是要需要掌握一定的基础才能方便理解,如果平时只是做上层CURD开发的话,还是要回头捡捡以前的知识。
位操作(Bit Manipulation)是程序设计中对位模式或二进制数的一元和二元操作。在许多古老的微处理器上,位运算比加减运算略快,通常位运算比乘除法运算要快很多。在现代编程语言中,情况并非如此,很多编程语言的解释器都会对基本的运算进行了优化,因此我们在实际开发中可以不必做一些编译器已经帮我们做好的优化,而就写出代码本身所要表现的意思即可。位运算的问题,很多都很有技巧性,大家需要掌握一定的位运算的应用,达到融会贯通的目的。
程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算就是直接对整数在内存中的二进制位进行操作。 比如,and运算本来是一个逻辑运算符,但整数与整数之间也可以进行and运算。举个例子,6的二进制是110,11的二进制是1011,那么6 and 11的结果就是2,它是二进制对应位进行逻辑运算的结果(0表示False,1表示True,空位都当0处理)。
1 1 0
and
1 0 1 1
----------------
0 0 1 0 = 2
各位运算在C、C++、Java中的表示方式,其中a、b都是整数。
| 含义\语言 | C/C++语言 | Java |
|---|---|---|
| 按位与 | a & b | a & b |
| 按位或 | a | b | a | b |
| 按位异或 | a ^ b | a ^ b |
| 按位取反 | ~a | ~a |
| 左移 | a << b | a << b |
| 带符号右移 | a >> b | a >> b |
| 无符号右移 | / | a >>> b |

题目解析:用位运算代替乘除法,被除数等于商*除数 + 余数,我们除数使用2的k次方进行逼近,然后进行累加,最后即可得到商。
代码如下:
/**
* 位运算
*/
class Solution {
public int divide(int dividend, int divisor) {
if(dividend == 0){
return 0;
}
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE;
}
boolean neg;
// 异或计算符号是否相异
neg = (dividend ^ divisor) < 0;
long t = Math.abs((long) dividend);
long d= Math.abs((long) divisor);
int result = 0;
for (int i=31; i>=0; i--) {
if ((t>>i) >= d) {
result += 1 << i;
t -= d << i;
}
}
return neg ? -result : result;//符号相异取反
}
}

题目解析:先把两个二进制数对齐,然后循环相加两个字符串中相同长度的低位数部分,然后加上carry位,最后反转即可。
代码如下:
/**
* char "0" = 48 , "1" =49
*/
class Solution {
public String addBinary(String a, String b) {
StringBuilder res = new StringBuilder();
int i = a.length() - 1;
int j = b.length() - 1;
// 进位
int carry = 0;
while (i >= 0 || j >= 0) {
// 把两个二进制数对齐
int num1 = i >= 0 ? a.charAt(i) - 48 : 0;
int num2 = j >= 0 ? b.charAt(j) - 48 : 0;
int sum = num1 + num2 + carry;
carry = 0;
if (sum >= 2) {
sum = sum % 2;
carry = 1;
}
// 先添加后反转,可以减少数组移动时间,从而提高效率
res.append(sum);
i--;
j--;
}
if (carry == 1) {
res.append(1);
}
// 反转输出
return res.reverse().toString();
}
}

题目解析:知道格雷编码的生成原理,也就知道怎么解题了,见以下规则。
格雷编码
公式:G(i) = i ^ (i/2);
如 n = 3:
G(0) = 000,
G(1) = 1 ^ 0 = 001 ^ 000 = 001
G(2) = 2 ^ 1 = 010 ^ 001 = 011
G(3) = 3 ^ 1 = 011 ^ 001 = 010
G(4) = 4 ^ 2 = 100 ^ 010 = 110
G(5) = 5 ^ 2 = 101 ^ 010 = 111
G(6) = 6 ^ 3 = 110 ^ 011 = 101
G(7) = 7 ^ 3 = 111 ^ 011 = 100
代码如下:
/**
* 位运算
*/
class Solution {
public List grayCode(int n) {
List res = new ArrayList<>();
for (int i = 0; i < 1 << n; ++i) {
res.add(i ^ i >> 1);
}
return res;
}
}

题目解析:这是一道经典的位运算题,需要讲讲异或的概念了,异或:相同为0,不同为1。0与任意数异或等于原数。异或同一个数两次,原数不变。例如:0 ^ a = a,a ^ b = c,c ^ b = a。就像那,人来人往,只有单身狗会剩下。
代码如下:
/**
* 位运算
*/
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for (int n : nums) {
res ^= n;
}
return res;
}
}

题目解析:需要一个状态转换电路,使得一个数出现3次时能自动抵消为0,最后剩下的就是只出现1次的数。
有如下规律:
0 ^ x = x,
x ^ x = 0;
x & ~x = 0,
x & ~0 = x;
x第一次出现后,a = (a ^ x) &~b的结果为 a = x, b = (b ^ x) & ~a的结果因为此时因为a = x了,所以b = 0。
x第二次出现:a = (a ^ x) & ~b, a = (x ^ x) & ~0, a = 0; b = (b ^ x) & ~a 化简, b = (0 ^ x) & ~0 ,b = x。
x第三次出现:a = (a ^ x) & ~b, a = (0 ^ x) & ~x ,a = 0;b = (b ^ x) & ~a 化简, b = (x ^ x) & ~0 ,b = 0;所以出现三次同一个数,a和b最终都变回了0。
只出现一次的数,按照上面x第一次出现的规律可知a = x,b = 0;因此最后返回a。
代码如下:
/**
* 位运算
*/
class Solution {
public int singleNumber(int[] nums) {
int a = 0, b = 0;
for (int num: nums) {
a = (a ^ num) & ~b;
b = (b ^ num) & ~a;
}
return a;
}
}

题目解析:从低位往高位枚举 n 的每一位,将其倒序添加到翻转结果 rev 中。
代码如下:
/**
* 位运算
*/
public class Solution {
public int reverseBits(int n) {
int rev = 0;
for (int i = 0; i < 32 && n != 0; ++i) {
rev |= (n & 1) << (31 - i);
n >>>= 1;
}
return rev;
}
}

题目解析:与 1 << i 依次进行 & 运算,即可判断当前数字i位是否为1。
代码如下:
/**
* 位运算
*/
public class Solution {
public int hammingWeight(int n) {
int ret = 0;
for (int i = 0; i < 32; i++) {
if ((n & (1 << i)) != 0) {
ret++;
}
}
return ret;
}
}

题目解析:用右移的方式,找到最长公共前缀即可。
代码如下:
/**
* 位运算
*/
class Solution {
public int rangeBitwiseAnd(int m, int n) {
int shift = 0;
// 找到公共前缀
while (m < n) {
m >>= 1;
n >>= 1;
++shift;
}
return m << shift;
}
}

题目解析:如果一个数是2的幂,则它的二进制为100000…,1后n个零。
代码如下:
/**
* 位运算
*/
class Solution {
public boolean isPowerOfTwo(int n) {
if(n <= 0){
return false;
}
// 与减1的二进制做且运算,如果是2的幂,则结果一定为0
return ( n & (n-1)) == 0;
}
}

题目解析:异或操作,首先异或整个数组,剩下x和y的异或结果,然后找到结果二进制为1的位,此位能将数组分为两部分,一部分包括x,一部分包括y和其它出现两次的数,分别对x和y的异或结果求异或操作,即可得出结果。
代码如下:
/**
* 位运算
*/
class Solution {
public int[] singleNumber(int[] nums) {
int a= 0;
for (int num : nums) {
a ^= num;
}
int i = 0;
for (; i <31 ; i++) {
if ((a>>i&1)==1)
break;
}
int x=0, y = 0;
for (int num : nums) {
if ((num>>i&1)==0)
x^=num;
else
y^=num;
}
return new int[]{x,y};
}
}

题目解析:还是异或操作,使用异或操作的特性:相同为0,不同为1。0与任意数异或等于原数。异或同一个数两次,原数不变。
代码如下:
/**
* 位运算
*/
class Solution {
public int missingNumber(int[] nums) {
int res = 0, index = 1;
for (int num : nums)
res ^= num ^ index++;
return res;
}
}
[xxxxx]