编程之美:位运算应用集萃
Bits are everything.
引子
位是程序世界的本源。一切软件计算不过是位的相加与复制。位向量(位的顺序数组)的空间效率以及位运算的高效,使得位运算深受资深程序员的喜爱。看见几行位运算,不由得会生出几分崇敬之情。本文来盘点一下位运算的应用。
基本操作
位的基本操作包括:
- 位的检测与置位:检测位是否为 0 或 1 , 将某一位置 0 或 置 1。通常用于位向量中,可以用来排序或者过滤。
- 与或非、异或。与或非运算通常用来实现掩码,获取某个或多个标识位;异或可用于计算、去重、交换。
- 算术左移或算术右移,逻辑左移或逻辑右移:结合与或非运算,用于定位某个位。
public class BitOp {
// 取 n 的第 m 位
public static int getBit(int n, int m){
return (n >> (m-1)) & 1;
}
// 将 n 的第 m 位置 1
public static int setBitToOne(int n, int m){
return n | (1 << (m-1));
}
// 将 n 的第 m 位置 0
public static int setBitToZero(int n, int m){
return n & ~(1 << (m-1));
}
public static String toBinaryString(int n) {
StringBuilder s = new StringBuilder();
for (int i=32; i >0; i--) {
s.append(getBit(n, i));
}
return s.toString();
}
static class BitOpTester {
public static void main(String[]args) {
test(999);
test(-999);
}
}
public static void test(int n) {
testUnit(n);
int n1 = setBitToOne(n, 29);
testUnit(n1);
int n2 = setBitToZero(n, 6);
testUnit(n2);
}
public static void testUnit(int num) {
String standardBinaryStr = Integer.toBinaryString(num);
System.out.println("Standard: " + standardBinaryStr);
String myOwnBinaryStr = toBinaryString(num);
System.out.println("My Own: " + myOwnBinaryStr);
assert standardBinaryStr.equals(myOwnBinaryStr);
}
}
获取不小于 cap 的 2 的幂次数(java.util.HashMap):
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
状态位组合标识
位的最常见使用,莫过于状态位组合标识了。 每个位代表某个含义,而这些含义是可组合的。
文件权限
Linux 系统的文件读/写/执行权限,使用了三位来标识:
- 读取权限: r = 100
- 写入权限 w = 010
- 执行权限 x = 001
Java 文件读写权限封装在 FilePermission 这个类里。获取 mask 对应的读写执行权限描述在 getActions 这个方法里。
public final class FilePermission extends Permission implements Serializable {
private final static int EXECUTE = 0x1;
private final static int WRITE = 0x2;
private final static int READ = 0x4;
private final static int DELETE = 0x8;
private final static int READLINK = 0x10;
private final static int NONE = 0x0;
private transient int mask;
private final static int ALL = READ|WRITE|EXECUTE|DELETE|READLINK;
方法标识
Java 方法的修饰符标识是采用位来标识。
public static final int PUBLIC = 0x00000001;
public static final int PRIVATE = 0x00000002;
public static final int PROTECTED = 0x00000004;
public static final int STATIC = 0x00000008;
public static final int FINAL = 0x00000010;
public static final int SYNCHRONIZED = 0x00000020;
public static final int VOLATILE = 0x00000040;
public static final int TRANSIENT = 0x00000080;
public static final int NATIVE = 0x00000100;
public static final int INTERFACE = 0x00000200;
public static final int ABSTRACT = 0x00000400;
public static final int STRICT = 0x00000800;
要知道某位是否存在,可以使用与运算,因为与运算有检测位的作用。
public static boolean isStrict(int mod) {
return (mod & STRICT) != 0;
}
要知道有哪些组合含义,可以使用或运算,因为或运算有保留位的作用。
/**
* The Java source modifiers that can be applied to a class.
* @jls 8.1.1 Class Modifiers
*/
private static final int CLASS_MODIFIERS =
Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE |
Modifier.ABSTRACT | Modifier.STATIC | Modifier.FINAL |
Modifier.STRICT;
联合或运算与左移或右移操作,可以用来对多个位进行拼接,比如 SnowFlake 算法里的实现(可参阅 “Snowflake Java”):
/**
* 起始的时间戳
*/
private final static long START_STMP = 1480166465631L;
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATACENTER_BIT = 5;//数据中心占用的位数
/**
* 每一部分的最大值
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId; //数据中心
private long machineId; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳
/**
* 产生下一个ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
标签
在应用程序里,常常用位的枚举来实现标签作用。
打包数据
比如 Java 线程池实现 ThreadPoolExecutor 将线程池状态和工作线程数打包在一个整数里。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
检测存在性
位图排序
位图可以将大量 key 映射到一个占用空间较少的位向量上,从而根据位向量来判断 key 的存在性。一个典型应用是,可以用来对稠密的很大的不重复整数列表进行排序,可节省不少空间。位图实现可阅 “位图排序(位图技术应用)”。
由于 Redis 提供了位数组功能,实现不重复数组排序的代码如下所示:
public void sort(int[] list) {
for (int i: list) {
jedis.setbit("arr", i, true);
}
int max = max(list);
for (int i=0; i <= max; i++) {
if (jedis.getbit("arr", i)) {
System.out.printf(String.format("%d ", i));
}
}
}
布隆过滤器
可以使用位运算来实现布隆过滤器。可以确定性地判断某个 key 不在某个集合里。布隆过滤器使用多个哈希函数,将 key 映射在多个位上(将相应位置一)。查找 key 时,如果发现某个位为 0 ,则表示该 key 不存在。
com.google.common.hash 包下的 BloomFilter 和 BloomFilterStrategies 给出了一个实现。BloomFilter 主要是起封装和入口作用,而 BloomFilterStrategies 是核心实现。注意到,由于 bitArray 是 long 型数组,因此使用 data[index >> 6] & (1L << index)
来获取 index 在这个 long 型数组所代表的位向量中的位置。这里给出了在一个整数数组代表的位向量里如何定位某个位的技巧。
enum BloomFilterStrategies implements BloomFilter.Strategy {
/**
* See "Less Hashing, Same Performance: Building a Better Bloom Filter" by Adam Kirsch and
* Michael Mitzenmacher. The paper argues that this trick doesn't significantly deteriorate the
* performance of a Bloom filter (yet only needs two 32bit hash functions).
*/
MURMUR128_MITZ_32() {
@Override public <T> boolean put(T object, Funnel<? super T> funnel,
int numHashFunctions, BitArray bits) {
long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
boolean bitsChanged = false;
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
bitsChanged |= bits.set(nextHash % bits.bitSize());
}
return bitsChanged;
}
@Override public <T> boolean mightContain(T object, Funnel<? super T> funnel,
int numHashFunctions, BitArray bits) {
long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
if (!bits.get(nextHash % bits.bitSize())) {
return false;
}
}
return true;
}
};
// Note: We use this instead of java.util.BitSet because we need access to the long[] data field
static class BitArray {
final long[] data;
int bitCount;
BitArray(long bits) {
this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]);
}
// Used by serialization
BitArray(long[] data) {
checkArgument(data.length > 0, "data length is zero!");
this.data = data;
int bitCount = 0;
for (long value : data) {
bitCount += Long.bitCount(value);
}
this.bitCount = bitCount;
}
/** Returns true if the bit changed value. */
boolean set(int index) {
if (!get(index)) {
data[index >> 6] |= (1L << index);
bitCount++;
return true;
}
return false;
}
boolean get(int index) {
return (data[index >> 6] & (1L << index)) != 0;
}
/** Number of bits */
int bitSize() {
return data.length * Long.SIZE;
}
/** Number of set bits (1s) */
int bitCount() {
return bitCount;
}
BitArray copy() {
return new BitArray(data.clone());
}
/** Combines the two BitArrays using bitwise OR. */
void putAll(BitArray array) {
checkArgument(data.length == array.data.length,
"BitArrays must be of equal length (%s != %s)", data.length, array.data.length);
bitCount = 0;
for (int i = 0; i < data.length; i++) {
data[i] |= array.data[i];
bitCount += Long.bitCount(data[i]);
}
}
@Override public boolean equals(Object o) {
if (o instanceof BitArray) {
BitArray bitArray = (BitArray) o;
return Arrays.equals(data, bitArray.data);
}
return false;
}
@Override public int hashCode() {
return Arrays.hashCode(data);
}
}
}
哈希与取模
由于位运算非常高效,通常都会用在哈希与取模运算中。比如 HashMap 的哈希值映射:
hash = (h = key.hashCode()) ^ (h >>> 16); // 高位与低位异或,充分使用到高位和低位
index = (n - 1) & hash; // n = 2^m
异或运算用在 FNV 哈希算法里。可以在 GitHub 里搜索 FNV 得到各种语言的实现。 FNV 的一个 Java 实现可参见: “fnv-java”。
去重与交换
异或操作符合交换律、结合律和自反性质,可用于去重和交换。 a^a = 0, a^0 = a , abc = a(bc) = a(cb)。 这使得异或有一些妙用。
一个经典的面试题是:假如你有一个用 1001 个整数组成的数组,这些整数是任意排列的,但是你知道所有的整数都在 1 到 1000 之间(包括 1000 )、此外,除了一个数字出现两次外,其他的数字只出现了一次。使用异或的解法是:先将所有数异或一遍,再依次异或 1-1000 的数即可。 用个简单的例子,假设含有 [1,2,3,2] ,那么方法是: 1232123 = 2 重复数就是 2.
一个变形的面试题是:一个数组存放若干整数,一个数出现奇数次,其余数均出现偶数次,找出这个出现奇数次的数?实际上道出了上一题的本质。
异或可以用来交换两个变量。
a=a^b; b=a^b; a=a^b;
科学计算
补码
使用位运算实现科学计算,即是从位的角度来分析数值计算。绝大多数机器上,整数是通过补码来表示的。补码的定义如下:
加法
异或可以实现无进位加法。对于有进位的加法,可以分解为两部分:
- 无进位的部分:使用异或运算 a^b = c ;
- 有进位的部分:使用 (a & b) << 1 可以得到进位的和 d ;
- 相加:将 c 和 d 相加,这时候会递归调用加法运算。
如下代码所示:
// 位运算实现加法
int bitAdd(int a,int b)
{
if(b==0)
return a;
int sum = a^b;
int carry =(a&b)<<1;
return bitAdd(sum,carry);
}
对于平均值的计算,思路类似。只是进位 d 除以 2 正好等于 a & b。
// 平均值
public static int avg(int a, int b) {
return ((a ^ b) >> 1) + (a & b);
}
其它使用位运算进行一些计算的例子如下(由于字长是有限的,计算机表示的整数范围是有限的,因此计算要谨防溢出。在根据加减运算来判断大小时,也要谨防溢出导致的错误):
(x ^ y) >= 0; // 判断两个数的符号是否相同(实际上是高位异或,如果相同就是 0 ,不同就是 1)
x & 1 == 0 ? false : true; // 判断奇偶,即判断最低位是 0 还是 1
x & (x-1) ; // 可以去掉最右边的 1 ,计算某个数有多少个二进制位 1
// 绝对值,当取最小负整数时没有对应绝对值。
public static int abs(int n) {
return (n ^ (n >> 31)) - (n >> 31);
}
public static int max(int a, int b) {
if ((a^b) > 0) {
// 同符号时 a-b 不会溢出,避免错误
return b & ((a-b) >> 31) | a & (~(a-b) >> 31);
}
return ((a ^ (1 << 31)) < 0) ? a : b; // 不同符号取非负即可
}
public static int min(int a, int b) {
if ((a^b) > 0) {
// 同符号时 a-b 不会溢出,避免错误
return a & ((a-b) >> 31) | b & (~(a-b) >> 31);
}
return ((a ^ (1 << 31)) >= 0) ? a : b; // 不同符号取负即可
}
// 判断正负,即判断最高位是 0 还是 1
public static boolean isNotNegative(int x) {
return (x ^ (1 << 31)) >= 0 ? false : true;
}
小结
位是一切程序的起点。位运算的使用主要体现在位向量和位运算的高效上,广泛应用于科学计算、哈希计算、存在性检测、状态位组合标识等场景。掌握位运算,就像携带了一把匕首,能够有效解决不少问题。