Skip to content

位运算技巧与实战

给你两个整数,只用位运算实现加法。你能想到怎么做吗?

大多数人第一反应是 a + b,但如果我告诉你 CPU 底层就是这样算的呢?

为什么面试喜欢考位运算

位运算是计算机最底层的操作,直接作用在二进制位上。面试考察它,往往是想看:

  1. 你对计算机底层的理解——高级语言封装得太好,反而让人忘了底层原理
  2. 你对问题本质的洞察——位运算能用最简单的操作解决复杂问题
  3. 你思维的灵活性——同样一个问题,用位运算往往有巧妙的解法

七种基本位运算

符号名称核心用法
&清零、提取掩码
``
^异或翻转、无进位加法
~取反负数转换
<<左移乘以 2^n
>>无符号右移除以 2^n
>>>有符号右移算术右移

经典问题:只用位运算实现加法

题目:不用 +- 运算符,实现两个整数相加。

这是位运算的经典题,核心思想来自二进制加法的本质:

  1. 无进位加法a ^ b(异或)
  2. 进位a & b << 1(与后左移一位)
  3. 递归或循环,直到进位为 0
java
public int add(int a, int b) {
    while (b != 0) {
        // 异或:计算无进位的加法结果
        int sum = a ^ b;
        // 与后左移:计算进位
        int carry = (a & b) << 1;
        a = sum;
        b = carry;
    }
    return a;
}

为什么这样能算加法?

二进制加法的每一步:同位相加,结果是异或,产生进位是「与」运算。反复迭代,直到没有进位,就是最终结果。

经典问题:只出现一次的数字

题目:给定一个数组,除了一个数字只出现一次外,其他数字都出现两次。找出那个只出现一次的数字。

java
public int singleNumber(int[] nums) {
    int result = 0;
    for (int num : nums) {
        result ^= num; // 异或运算
    }
    return result;
}

为什么异或能找到?

  1. a ^ a = 0(相同为 0)
  2. a ^ 0 = a(异或 0 不变)
  3. 异或满足交换律和结合律

所以所有成对的数字异或后都变成 0,只剩那个落单的。

进阶:只出现一次的数字 II

题目:其他数字都出现三次,只有一个数字出现一次。找出那个数字。

这次不能再用简单异或了——因为 a ^ a ^ a = a,三次会变回自身。

思路:统计每一位上 1 的个数,模 3 后剩下的就是目标数字该位

java
public int singleNumber(int[] nums) {
    int result = 0;
    // int 有 32 位,统计每一位上 1 的个数
    for (int i = 0; i < 32; i++) {
        int bitCount = 0;
        int mask = 1 << i; // 当前位的掩码
        for (int num : nums) {
            // 判断 num 的第 i 位是否为 1
            if ((num & mask) != 0) {
                bitCount++;
            }
        }
        // 如果出现 3 次后余 1,说明目标数字该位是 1
        if (bitCount % 3 != 0) {
            result |= mask;
        }
    }
    return result;
}

为什么取模 3?

因为相同数字都出现 3 次,每一位上 1 的总数一定是 3 的倍数。如果有余数 1,说明目标数字该位是 1。

经典问题:二进制中 1 的个数

题目:统计一个整数二进制中 1 的个数。

方法一:右移位

java
public int hammingWeight(int n) {
    int count = 0;
    while (n != 0) {
        count += n & 1; // 判断最低位是否为 1
        n >>>= 1; // 无符号右移
    }
    return count;
}

方法二:n & (n-1) 消除最低位的 1

java
public int hammingWeight(int n) {
    int count = 0;
    while (n != 0) {
        n &= (n - 1); // 消除最低位的 1
        count++;
    }
    return count;
}

为什么 n & (n-1) 能消除最低位的 1?

n-1 会将 n 最右边的 1 变成 0,其右边所有位变成 1。两者相与,原来最低位的 1 就被消掉了。

经典问题:判断是否是 2 的幂

java
public boolean isPowerOfTwo(int n) {
    // 2 的幂:二进制只有一个 1
    // n > 0 && (n & (n-1)) == 0
    return n > 0 && (n & (n - 1)) == 0;
}

为什么 n & (n-1) == 0 能判断?

2 的幂的二进制只有一个 1(如 8 = 1000)。消除这个 1 后变成 0,所以 n & (n-1) == 0

经典问题:交换两个整数(不使用临时变量)

java
public void swap(int a, int b) {
    a = a ^ b; // a 变成 a^b
    b = a ^ b; // b = (a^b)^b = a
    a = a ^ b; // a = (a^b)^a = b
}

为什么异或能交换?

利用异或的性质:x ^ x = 0x ^ 0 = x

  • 第一步:a = a ^ b
  • 第二步:b = (a ^ b) ^ b = a
  • 第三步:a = (a ^ b) ^ a = b

注意:如果 a 和 b 指向同一块内存(如 swap(arr[i], arr[i])),会变成 0。

位运算实战技巧

掩码操作

java
// 获取第 i 位的值(0 或 1)
int getBit(int num, int i) {
    return (num >> i) & 1;
}

// 设置第 i 位为 1
int setBit(int num, int i) {
    return num | (1 << i);
}

// 设置第 i 位为 0
int clearBit(int num, int i) {
    return num & ~(1 << i);
}

// 将第 i 位到第 j 位都置为 1
int updateBits(int num, int i, int j, int newBits) {
    int mask = ~0; // 全 1 的掩码
    mask = mask << (j - i + 1); // 左移到 j 位置
    mask = mask >>> (j - i + 1); // 右移回来,左边补 0
    // ... 更多细节省略
}

常用技巧

java
// 判断奇偶:最后一位决定
boolean isOdd(int n) {
    return (n & 1) == 1;
}

// 乘以 2 的幂
int multiplyPowerOfTwo(int n, int k) {
    return n << k;
}

// 除以 2 的幂
int dividePowerOfTwo(int n, int k) {
    return n >> k;
}

// 取绝对值(考虑负数最小值的边界情况)
int abs(int n) {
    int sign = n >> 31; // 负数全 1,正数全 0
    return (n ^ sign) - sign; // 异或后减 sign
}

位运算小结

技巧应用
n & (n-1)消除最低位的 1
n & (-n)获取最低位的 1
n ^ n = 0消除成对出现的数
a ^ b ^ a = b交换两个变量
a & (~0 << k)清除低 k 位

面试追问预判

  1. Java 中 >>>>> 的区别?

    • >> 是算术右移,符号位扩展(负数高位补 1)
    • >>> 是逻辑右移,高位补 0
  2. 如何判断一个数是否为负数(只用位运算)?

    • n >> 31 == -1(对于 32 位 int)
  3. 为什么负数的二进制表示是补码?

    • 补码让加减法统一处理,不需要单独的减法器

记忆口诀

异或相同得 0,异或 0 不变。n & (n-1) 消 1,n & (-n) 取 1。移位代替乘除,掩码提取位。

位运算看起来神秘,用熟了就会发现它其实就是「二进制世界的加减乘除」。

基于 VitePress 构建