一、引言
学计算机最早一般会了解 Byte(字节) 和 Bit(位) 的区别。笔者还还得当年研究生面试的时候,导师看我是跨专业的比较担心,还真的问到我这个问题。
位运算在面试中不多见,但是在算法中用到往往会加分比如本文例子中用来记录选择。比起其他高级数据结构比如 Set、Array 和 List,空间复杂性更友好。
二、基本运算
位运算的与、或、非、异或、取反这里就不具体说明,记得不要跟逻辑运算符搞混就好了。另外负数在计算机中通过补码来表示(取反+1)。
下面是一些位运算基本的用法。
- 判断奇偶性: X & 1 == 1 => 奇数,== 0 => 偶数
- 把最低位的 1 清 0 => X = X & (X-1)
- 得到最低位的 1 比如 1100 返回 100 => X & -X
- 把 X 最右边的 n 位清 0 => X & (~0 << n)
- 获取 X 的第 n 位值 => (X >> n) & 1
- 获取 X 的第 n 位的幂值 => X & (1 << (n-1)) ???
- 仅将第 n 位置为 1 => X | (1 << n)
- 仅将第 n 位置为 0 => X & (~(1 << n))
- 将 X 最高位至 n 位(含)清 0 => X & ((1 << n) - 1)
- 将第 n 位至第 0 位(含)清 0 => X & (~((1 << (n + 1)) - 1))
- 异或特性 X ^ 0 = X X ^ 1(全 1) = ~X(取反),1(全 1)= ~0 X ^ (~X) = 1(全 1) X ^ X = 0 交换率 => a ^ b = c => a ^ c = b, b ^ c = a 结合律 => a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c
三、解决问题例子
这里举一个求解八皇后所有解法个数的问题。国际象棋中的八皇后问题指的是在一个 8 X 8 的棋盘上,每行摆一个皇后,不冲突的情况下求一共有多少种摆法。皇后能攻击在同一行、同一列、正斜线、反斜线上的所有其他棋子(包括皇后),所以这是约束条件:摆放好的八皇后不能在同一行、同一列、正斜线和反斜线上。
八皇后简单解法是回溯法。每行有 8 种选择(从第1到第8列),如果皇后之间没有冲突则递归调用到下一行,直到最后一行找到一个可行解,否则提前退出回溯其他选择。
采用位运算也可以求解所有的解法。
package org.stone.study.algo.ex202402;
import java.util.Arrays;
/**
* 分别采用位运算和回溯法求解八皇后的所有解法个数问题
*/
public class EightQueen {
public EightQueen(int n) {
this.n = n;
}
public static void main(String[] args) {
EightQueen eightQueen = new EightQueen(8);
// 八皇后所有解为92个
// int count = eightQueen.dfs1(0, 0, 0, 0);
int count = eightQueen.backtrack();
System.out.println("count:" + count);
}
/**
* 位运算方式计算八皇后的解法个数
* @param row:当前行数
* @param col:一个整数表示所有列上放置皇后的情况
* @param pie:一个整数表示所有斜线上放置皇后的情况
* @param na:一个整数表示所有反向斜线上放置皇后的情况
*/
public int dfs1(int row, int col, int pie, int na) {
int count = 0;
if(row == n) {
return 1;
}
// 一共有多少个位置可以放
int bits = (~(col | pie | na)) & ((1 << n) - 1);
while(bits > 0) {
// 取最后一个 1 表示的数
int bit = bits & -bits;
// pie 下一行左移1位的位置已经被占用了,na 下一行右移1位的位置也被占用了
count += dfs1(row + 1, col | bit, (pie | bit) << 1, (na | bit) >> 1);
// 这个位置已经被选择过了,需要去掉
bits = bits & (bits - 1);
}
return count;
}
/**
* 回溯法求解 N 皇后的解法个数
* @return
*/
public int backtrack() {
int[][] board = new int[8][8];
for(int i = 0; i < 8; i++) {
Arrays.fill(board[i], 0);
}
return dfs2(board, 0);
}
public int dfs2(int[][] board, int row) {
if(row == n) {
return 1;
}
int count = 0;
for(int j = 0; j < n; j++) {
//剪枝 + 回溯
if(validMove(board, row, j)) {
board[row][j] = 1;
count += dfs2(board, row + 1);
board[row][j] = 0;
}
}
return count;
}
/**
* 判断当前走的一步是否合法:列上没有皇后;左上斜线没有皇后;右上斜线没有皇后
* @param board
* @param row
* @param col
* @return
*/
private boolean validMove(int[][] board, int row, int col) {
// 同一列上不能有皇后
for(int i = 0; i < row; i++) {
if(board[i][col] == 1) return false;
}
// 左上的反斜线上不能有皇后
for(int i = row - 1, j = col -1; i >= 0 && j >= 0; i--,j--) {
if(board[i][j] == 1) return false;
}
// 右上的反斜线上不鞥有皇后
for(int i = row -1, j = col + 1; i >= 0 && j < n; i--, j++) {
if(board[i][j] == 1) return false;
}
return true;
}
}
四、总结
位运算在算法中是比较 tricky 的存在,比如判断奇偶性、乘法等能提高算法运行的效率。更复杂一点的算法中,通过 bit 位记录所有可能的选择,再依次遍历判断选择是否是合法解,会让算法理解起来简单,空间占用更少,这里不展开。
应用层面使用位运算的例子有布隆过滤器,Redis 中的位图等。这些主要是提高空间复杂度为主,同时位运算对提高程序性能也更友好。