位运算应用

位运算应用

1. 基础运算

1.1. 奇偶判断

真实世界中一般用求余2来判断奇偶, 转换成代码如下:

boolean isOdd(int num) {
    return num % 2 == 1;
}

可以直接替换成位运算:

boolean isOdd(int num) {
    return (num & 1) == 1;
}

奇数的最低位必然是1.

1.2. 统计1的数量

统计二进制数上有多少个1.

// openjdk17的Integer.bitCount
public static int bitCount(int i) {
    // HD, Figure 5-2
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}

1.3. 取符号位

正数为1, 负数为-1, 0为0.

// 来自openjdk17的Integer.signum
public static int signum(int i) {
    // HD, Section 2-7
    return (i >> 31) | (-i >>> 31);
}

1.4. 大小写变换

小写转大写: ch |= 32, 大写转小写: ch &= ~32, 大写转小写, 小写转大写: ch ^= 32

1.5. 求数组中唯一一个出现次数为奇数的数字

数组中有一个数字只出现了奇数次, 其它数字都出现了偶数次, 可以通过异或找到这个数字.

利用异或的特性: a ^ a = 0, a ^ 0 = a

public int findOddTimeNum(int[] arr) {
	var num = 0;
    for(var i : arr) {
        num ^= i;
    }
    return num;
}

2. 取大于等于正整数n的最小的2的整数次幂

例如, 3的最小2的整数次幂是4, 5的是8, 等等.

这个算法在jdk中的HashMap中有使用到.

实现方式至少有两种.

2.1. 高位右移取或

我们假设最高位的1在第k位上(从右往左数从1开始), k=28, 二进制如: 0b00001..., 这个最高位1的左侧全是0, 右侧位的值则不重要.

将其右移一位之后得到0b000001..., 两个值或之后得到: 0b000011..., 可以发现, 此时第28位与27位变成了1, 我们可以再右移两位进行同样的计算, 可以得到: 0b00001111..., 循环之前的做法, 最终我们能得到一个k右侧所有位都是1的数字, 即 2n - 1, 此时再加上1就得到了需要的数字.

因为int是32位数, 所以只需要分别执行移位1, 2, 4, 8, 16次即可.

注意如果一开始的数字就是2的整数幂, 我们期望得到他自己, 可以将其减一之后, 再进行运算.

代码如下:

int minPowerOf2(int num) {
	--num;
    num |= num >> 1;
    num |= num >> 2;
    num |= num >> 4;
    num |= num >> 8;
    num |= num >> 16;
    return ++num;
}

考虑到溢出问题, 最好再规定一个最大值, 大于这个值的数可以抛出异常, 也可以使用该最大值返回.

static int MAX = 1 << 30;
int minPowerOf2(int num) {
    if (num <= 0)
        throw new IllegelArgumentException("must great than zero");
    if(num >= MAX) {
        return MAX;
    }
	--num;
    num |= num >> 1;
    num |= num >> 2;
    num |= num >> 4;
    num |= num >> 8;
    num |= num >> 16;
    return ++num;
}

2.2. 计算前缀0

统计前缀0的数量, 然后将-1(二进制所有位都是1)无符号右移前缀0的数量再加一.

统计前缀0的方法是二分处理, 此处不做分析, 参考文档: https://www.cnblogs.com/xiepl1997/p/13479769.html

static int MAX = 1 << 30;
public static int minPowerOf2(int num) {
    if (num <= 0)
        throw new IllegelArgumentException("must great than zero");
    if(num >= MAX) {
        return MAX;
    }
    int n = -1 >>> numberOfLeadingZeros(num - 1);
    return n + 1;
}
// 来自openjdk17的Integer.numberOfLeadingZeros
public static int numberOfLeadingZeros(int i) {
    if (i <= 0)
        return i == 0 ? 32 : 0;
    int n = 31;
    if (i >= 1 << 16) { n -= 16; i >>>= 16; }
    if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
    if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
    if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
    return n - (i >>> 1);
}

3. 权限开关和标志位

使用位运算做权限有个经典的例子: linux系统的文件权限.

可执行, 可写, 可读的权限被分别设置为0b001, 0b010, 0b1001, 2, 4, 由这几个数字的有无可以组成0-7内的任意一个数字, 共有八种权限组成.

标志位的逻辑与权限本质是一样的, 只是换了个名称.

一般代码类似下面这样:

int allPermission;

void addPerminssion(int permission) {
    allPermission |= permission;
}

void removePermission(int permission) {
    allPermission &= ~permission;
}

boolean hasPermission(int permission) {
    return (allPermission & permission) == permission;
}

4. 异或加密

利用异或的性质: 明文 ^ 密钥 = 密文, 密文 ^ 密钥 = 明文

这种加密方式并不安全, 因为明文 ^ 密文 = 密钥, 但是便于实现且相当简单.

参考文档:

http://www.ruanyifeng.com/blog/2017/05/xor.html

https://www.cnblogs.com/GLory-LTF/p/15177221.html

// 一个字节加密所有数据
public static void xorCipher(byte[] message, byte key) {
    for(var i = 0; i < message.length; ++i) {
        message[i] ^= key;
    }
}

// 长度相当或key更长的加密
public static void xorCipher(byte[] message, byte[] key) {
    if(message.length > key.length) {
        throw new IllegelArgumentException("key长度不足");
    }
    for(var i = 0; i < message.length; ++i) {
        message[i] ^= key[i];
    }
}

5. 逻辑运算

假设1代表true, 0代表false, 可以使用位运算来代替逻辑运算符, 值得注意的是, 逻辑运算符可以立即返回但是位运算符不是.

在某些语言中, 位运算符也能作为逻辑运算符, 但是不具有"短路"特征, 例如java. 而对于某些语言, 可以同时表达位运算和逻辑运算, 例如javascript.

&的位运算与逻辑与&&相同.

|的位运算与逻辑或||相同.

expression ^ 1的位运算与!booleanExpression相同.

6. 版本号

通常人类友好阅读的版本格式采用三个数字, 以.分隔, 分别表示大版本(完全不兼容更新), 中版本(部分api变更或新增重要功能), 小版本(bug修复).

这种形式同样也可以采用位运算转换, 将这种字符串以数字进行保存.

例如, 将int32以8位分割, 高8位保留, 然后从高位到低位, 每8位分别表示大版本, 中版本, 小版本.

这样能每个版本能划分出256个版本, 对于大部分项目和框架都足以应付, 如果有需要, 可以灵活增加相应版本位数, 每增加1位, 版本号将增加一倍.

例如下面的代码实现了从x.y.z到对应数字版本的实现.

public Integer convertToDatabaseColumn(String attribute) {
    if (attribute == null) {
      return null;
    }
    String[] version = attribute.split("\\.");
    return (Integer.parseInt(version[0]) << 16)
      + (Integer.parseInt(version[1]) << 8)
      + Integer.parseInt(version[2]);
}
public String convertToEntityAttribute(Integer dbData) {
    if (dbData == null) {
      return null;
    }
    return String.format(
      "%d.%d.%d", (dbData >> 16) & 0xFF, (dbData >> 8) & 0xFF, dbData & 0xFF);
}
posted @ 2022-08-16 17:19  无以铭川  阅读(76)  评论(0)    收藏  举报