GESP 5级

位运算 与/或/非/异或

5级 · 基础算法与数据类型

📖 什么是位运算?

我们平时用的 +、-、*、/ 是对数值进行运算。而位运算是直接对数字的二进制表示中的每一位(bit)进行运算。

打个比方:普通运算是"按计算器",位运算则是"直接操作计算器里的电路开关"——更底层、更快、更巧妙。

C++ 提供四种基本位运算符:

& 按位与(AND) — 两位都为1才为1
| 按位或(OR) — 有一位为1就为1
^ 按位异或(XOR)— 不同为1,相同为0
~ 按位取反(NOT) — 0变1,1变0
🌟 为什么重要?
  • 速度极快:位运算是 CPU 最原生的操作,比乘除法快很多,在竞赛中用来"卡时间"。
  • 状态压缩:用一个整数的二进制位来表示集合的状态(比如"哪些物品被选中"),这就是"状态压缩 DP"的基础。
  • 交换两个数:不用临时变量就能交换两个数(a ^= b; b ^= a; a ^= b;),虽然面试炫技更多,但理解异或很重要。
  • 判断奇偶:x & 1 可以瞬间判断一个数是奇数还是偶数,比 x % 2 更高效。
  • 清零最低位:x & (x-1) 可以把 x 二进制中最右边的 1 变成 0,这是很多算法题的巧妙技巧。
📋 前置知识
  • ✅ 了解二进制的表示方法(每个数可以用 0 和 1 的串来表示)
  • ✅ 十进制和二进制的互相转换(参考"任意进制转换"页面)
  • ✅ 基本的 C++ 语法(变量、输出、循环)

🔢 & 按位与(AND)

规则:两个位都为 1,结果才为 1;否则为 0。

生活类比:想象两个人投票,两个人都同意才通过——这就是"与"。

📐 运算规则
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

示例:12 & 10

12 的二进制:1 1 0 0
10 的二进制:1 0 1 0
─────────────────
& 运算结果:1 0 0 0 = 8

常见用途:x & 1 判断奇偶——如果结果是 1 就是奇数,0 就是偶数。因为只有最后一位是 1 时结果才为 1。

🔢 | 按位或(OR)

规则:两个位中至少有一个为 1,结果就为 1;全为 0 才是 0。

生活类比:两个人投票,只要有一个人同意就通过——这就是"或"。

📐 运算规则
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1

示例:12 | 10

12 的二进制:1 1 0 0
10 的二进制:1 0 1 0
─────────────────
| 运算结果:1 1 1 0 = 14

常见用途:x | (1 << k) 把 x 的第 k 位设为 1。比如要标记"第 3 个物品被选中",就可以用或运算。

🔢 ^ 按位异或(XOR)

规则:两个位不同为 1,相同为 0。

生活类比:两个人意见不同时才算通过,意见相同时不通过——这就是"异或"。

📐 运算规则
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0

示例:12 ^ 10

12 的二进制:1 1 0 0
10 的二进制:1 0 1 0
─────────────────
^ 运算结果:0 1 1 0 = 6

异或的神奇性质:

① x ^ x = 0(自己异或自己 = 0)
② x ^ 0 = x(异或 0 不变)
③ a ^ b = b ^ a(交换律)
④ (a ^ b) ^ c = a ^ (b ^ c)(结合律)

用性质①②可以找"只出现一次的数":
把所有数异或起来,成对出现的都抵消了,只剩下那个唯一的数。

🔢 ~ 按位取反(NOT)

规则:把每一位翻转,0 变 1,1 变 0。

就像"拍照反色"一样——黑色变白色,白色变黑色。

注意:在 C++ 中,~0 的结果是 -1(因为负数用补码表示),初学者不需要深究补码,知道取反的逐位操作即可。

💻 综合代码示例

💻 完整演示
#include <iostream>
#include <bitset>  // 用来把整数显示为二进制,方便观察
using namespace std;

int main() {
    int a = 12;  // 二进制: 1100
    int b = 10;  // 二进制: 1010

    // 输出原始值和二进制表示
    cout << "a = " << a << " (二进制: " << bitset<8>(a) << ")" << endl;
    cout << "b = " << b << " (二进制: " << bitset<8>(b) << ")" << endl;

    // 1. 按位与 &:两位都1才1
    cout << "a & b = " << (a & b) << endl;   // 输出: 8  (二进制: 1000)

    // 2. 按位或 |:有一位1就1
    cout << "a | b = " << (a | b) << endl;   // 输出: 14 (二进制: 1110)

    // 3. 按位异或 ^:不同为1,相同为0
    cout << "a ^ b = " << (a ^ b) << endl;   // 输出: 6  (二进制: 0110)

    // 4. 按位取反 ~:0变1,1变0
    cout << "~a = " << (~a) << endl;         // 输出: -13

    // ====== 常用技巧 ======

    // 技巧1:判断奇偶
    int x = 7;
    if (x & 1) {
        cout << x << " 是奇数" << endl;  // 会执行这行
    } else {
        cout << x << " 是偶数" << endl;
    }

    // 技巧2:交换两个数(不需要临时变量)
    int m = 5, n = 9;
    cout << "交换前: m=" << m << ", n=" << n << endl;
    m ^= n;  // m = m ^ n
    n ^= m;  // n = n ^ (m ^ n) = n ^ n ^ m = m
    m ^= n;  // m = (m ^ n) ^ m = n
    cout << "交换后: m=" << m << ", n=" << n << endl;

    // 技巧3:找只出现一次的数
    // 数组 [2, 3, 5, 3, 2] 中,5 只出现了一次
    int arr[] = {2, 3, 5, 3, 2};
    int result = 0;
    for (int i = 0; i < 5; i++) {
        result ^= arr[i];  // 全部异或,成对的抵消
    }
    cout << "只出现一次的数: " << result << endl;  // 输出: 5

    return 0;
}

⚠️ 易错点

🚨 常见错误
  1. 位运算和逻辑运算搞混:& 是"按位与"(逐位操作),&& 是"逻辑与"(只看真假)。3 & 1 = 1,而 3 && 1 = 1——虽然结果一样,但含义完全不同。对于 2 & 1 = 0 但 2 && 1 = 1,就完全不一样了。
  2. 运算符优先级:位运算的优先级比比较运算符低!a & b == 0 实际上是 a & (b == 0),要用括号:(a & b) == 0。
  3. 异或交换的写法顺序:三行必须按顺序写,顺序错了结果就不对:m ^= n; n ^= m; m ^= n;
  4. ~ 取反的结果:~0 不是 -1 的"绝对值取反",而是二进制全 1,在 int 下是 -1。~1 是 -2,不要被负数吓到。
  5. 不要滥用位运算:位运算虽然快,但可读性差。竞赛中用没问题,平时写代码还是优先用清晰的写法,除非性能真的不够。

📝 练习建议

💡 怎么练?
  1. 手写二进制验证:每次做位运算题,先在纸上写出两个数的二进制,逐位运算,再和程序对比。
  2. 背熟异或的性质:x^x=0、x^0=x 是解题的核心。很多"找唯一数"的题都靠这个。
  3. 练习"只出现一次的数":LeetCode 136 是经典入门题,数组里其他数都出现两次,找那个只出现一次的。
  4. 练习状态压缩:用一个整数的二进制位表示集合,比如 5 = 101 表示"选了第0个和第2个物品"。
  5. 推荐题目:洛谷 P1461(二进制相关)、LeetCode 136/191/338。