带你玩转位运算

72 阅读6分钟

最近刷到一个有趣视频指出 a = a ^ b ^ b,因此来研究研究位运算,可谓是“暗藏玄机”,十分 Interesting 且实用。

1. 异或 (XOR ^)

1.1 基本定义

异或(XOR,exclusive OR)是一种逻辑运算,符号通常为 ^。它的运算规则是:相同为 0,相异为 1

数学表达式:

  • 0 ^ 0 = 0
  • 0 ^ 1 = 1
  • 1 ^ 0 = 1
  • 1 ^ 1 = 0

1.2 数学性质

异或运算具有以下重要数学性质:

  1. 交换律a ^ b = b ^ a
  2. 结合律(a ^ b) ^ c = a ^ (b ^ c)
  3. 自反性a ^ a = 0
  4. 恒等性a ^ 0 = a
  5. 可逆性:如果 a ^ b = c,则 a ^ c = bb ^ c = a

1.3 在编程中的应用

1.3.1 简单加解密与混淆

用于简单的对称加解密,以及订单系统中自增号部分的混淆。 原理明文 ^ 密钥 = 密文密文 ^ 密钥 = 明文推导: 利用结合律和自反性:a ^ b ^ b = a ^ (b ^ b) = a ^ 0 = a

代码示例 (Python):

def xor(data, key):
    result = []
    for i in range(len(data)):
        k = key[i % len(key)]   
        result.append(data[i] ^ k)
    return bytes(result)

1.3.2 不使用临时变量交换两个整数

推导

  1. a = a ^ b
  2. b = a ^ b → 代入得 (a ^ b) ^ b = a (此时 b 变成了原 a)
  3. a = a ^ b → 代入得 (a ^ b) ^ a = b (此时 a 变成了原 b)

代码示例 (Python):

def swap_without_temp(a, b):
    a ^= b
    b ^= a
    a ^= b
    return a, b

print(swap_without_temp(5, 10))  # 输出: (10, 5)

1.3.3 只出现一次的数字(LeetCode 136)

用于查找只出现一次的数字、成对缺失查找、数据去重等。 原理:利用 a ^ a = 0a ^ 0 = a

代码示例 (Python):

def find_single_number(nums):
    result = 0
    for num in nums:
        result ^= num
    return result

# 示例
print(find_single_number([4, 1, 2, 1, 2]))  # 输出: 4

2. 或 (OR |)

2.1 基本定义

或(OR,inclusive OR)是一种基本的逻辑运算,符号通常为 |运算规则:只要有一个操作数为 1,结果即为 1;仅当所有操作数为 0 时,结果为 0。

真值表:

ABA | B
000
011
101
111

2.2 数学性质

  • 交换律a | b = b | a
  • 结合律(a | b) | c = a | (b | c)
  • 幂等性a | a = a
  • 恒等性a | 0 = a
  • 吸收性a | 1 = 1
  • 分配律(对与运算):a | (b & c) = (a | b) & (a | c)

2.3 在编程中的应用

2.3.1 权限控制(位掩码)

在 Linux 系统或业务系统中,常使用位掩码(bitmask)表示用户权限。 原理

读    = 001  (1)
写    = 010  (2)
执行  = 100  (4)

组合权限:
读写     = 读 | 写 = 001 | 010 = 011 (3)
读写执行 = 读 | 写 | 执行 = 111 (7)

代码示例 (Python):

# 定义权限标志
READ_PERMISSION = 0b001     # 1
WRITE_PERMISSION = 0b010    # 2
EXECUTE_PERMISSION = 0b100  # 4

# 为用户添加权限
def add_permission(user_perms, new_perms):
    return user_perms | new_perms

user_perms = 0
user_perms = add_permission(user_perms, READ_PERMISSION)   # 0b001
user_perms = add_permission(user_perms, WRITE_PERMISSION)  # 0b011

# 检查权限 (配合 & 使用)
def has_permission(user_perms, check_perm):
    return (user_perms & check_perm) == check_perm

print(has_permission(user_perms, READ_PERMISSION))     # True
print(has_permission(user_perms, EXECUTE_PERMISSION))  # False

2.3.2 实际监控场景

告警通知渠道配置(电话、邮件、短信)。 使用 OR 运算存储,节约空间,高效查询,避免使用数据库的 Like 查询,充分利用索引。

电话 = 1, 短信 = 2, 邮件 = 4
电话 + 短信 = 1 | 2 = 3
全渠道 = 1 | 2 | 4 = 7

2.3.3 多位数据的拼接(雪花算法)

如 64 位雪花算法 ID 的高效拼接。无需十进制相加,直接使用位移 << 和按位或 |

代码示例 (Java):

public class SimpleSnowflake {
    private static final long TIMESTAMP_BITS = 41L;
    private static final long MACHINE_ID_BITS = 10L;
    private static final long SEQUENCE_BITS = 12L;

    public long generateId(long timestamp, long machineId, long sequence) {
        // 技巧:使用 << 将各部分移动到指定位置,使用 | 将它们"拼"起来
        return (timestamp << (MACHINE_ID_BITS + SEQUENCE_BITS)) 
             | (machineId << SEQUENCE_BITS) 
             | sequence;
    }
}

2.3.4 最接近的 2 的幂次方数

Java 8 中 HashMap 容量的计算,确保容量永远是 2 的幂次方。

代码示例 (Java):

public class CapacityCalculator {
    // 例 new HashMap<>(7) 实际容量为 8
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        // 技巧:通过连续的无符号右移和按位或,将最高位 1 后面的所有位都变为 1
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        // 此时 n 类似于 000111...,最后 +1 即可得到 001000... (即2的幂)
        return n + 1;
    }
}

3. 与 (&)

3.1 基本定义

与运算(Logical AND),符号通常为 &运算规则:全为 1 时结果为 1,只要有一个 0 结果即为 0。

真值表:

ABA & B
000
010
100
111

3.2 数学性质

  • 交换律a & b = b & a
  • 结合律(a & b) & c = a & (b & c)
  • 幂等性a & a = a
  • 恒等性a & 1 = a (用于取最低位)
  • 支配性a & 0 = 0
  • 分配律(对或运算):a & (b | c) = (a & b) | (a & c)

3.3 在编程中的应用

3.3.1 检查是否 2 的幂(LeetCode 231)

原理n & (n - 1)。如果 n 是 2 的幂次方,二进制最高位为 1 其余为 0;n-1 则最高位变为 0 其余变为 1。两者相与必为 0。

代码示例 (Python):

def is_power_of_two(n):
    if n <= 0:
        return False
    return (n & (n - 1)) == 0

3.3.2 极其高效的“取模”运算

原理X % N == X & (N - 1) (仅当 N 为 2 的幂时成立)。 在分库分表、Hash 计算、负载均衡中广泛使用。

代码示例 (Java):

// 假设 Map 的容量是 16 (2的幂)
int capacity = 16;
int hash = "user_id_123".hashCode();

// 普通写法
int index1 = hash % capacity; 

// 高手写法 (位运算比取模快,且避免了负数 hash 取模的问题)
int index2 = hash & (capacity - 1); 

3.3.3 IP 网段的判断

判断一个 IP 是否属于某个 CIDR 网段。 原理:IP 地址和子网掩码(Mask)本质都是 32 位整数。

代码示例 (Java 风格):

// 判断 IP: 192.168.1.55 是否在 192.168.1.0/24 网段内
int ip = parseIp("192.168.1.55");
int mask = parseIp("255.255.255.0"); 
int network = parseIp("192.168.1.0");

// 计算该 IP 的网络号
if ((ip & mask) == network) {
    return true; // 在白名单网段内
}

4. 反 (~)

4.1 基本定义

按位翻转取反(NOT)。 真值表

  • ~0 = 1
  • ~1 = 0

4.2 在编程中的应用

4.2.1 权限移除

| 用于添加权限,& 用于检查权限,而 ~ 配合 & 则是移除权限的标准写法。

代码示例 (Java):

public class PermissionSystem {
    private static final int PERMISSION_READ   = 1 << 0; // 1
    private static final int PERMISSION_WRITE  = 1 << 1; // 2
    private static final int PERMISSION_DELETE = 1 << 2; // 4

    public static void main(String[] args) {
        // 1. 初始化权限:拥有 读、写、删除 (111)
        int userPermissions = PERMISSION_READ | PERMISSION_WRITE | PERMISSION_DELETE;

        // 2. 业务需求:剥夺用户的“删除”权限
        // 技巧:使用 &= ~MASK
        // 逻辑:~PERMISSION_DELETE 生成一个"删除位"为0,其余全为1的掩码
        userPermissions &= ~PERMISSION_DELETE;
        
        // 结果:userPermissions 变为 011 (只剩读和写)
    }
}

总结

作为后端开发人员,掌握位运算不仅能帮助我们解决特定的算法题,更能指导我们设计出更紧凑的数据结构和更高效的系统逻辑。

  • 异或 (^):用于加密、校验、无变量交换、数据去重。
  • 或 (|):用于状态叠加、权限组合、数据拼接。
  • 与 (&):用于状态检测、掩码提取、取模优化、数值性质判断。
  • 反 (~):用于状态移除、反转逻辑。