二进制暗号:当灯泡学会用开关说话
你有没有在夜晚走过一条走廊,灯一盏一盏亮起来,像在跟你打招呼?每一盏灯只有两种状态:亮或灭。计算机的世界也是这样——无数个开关,只有 0 和 1。但人类厉害的地方在于,我们不只是看着灯发呆,而是学会用灯的亮灭来“说话”、来“约定暗号”。
今天要聊的按位与 & 和按位或 |,就是最基础的两种“灯光暗号”。它们看起来简单到不行,但如果你真正理解它们,会发现很多看似复杂的位运算题目,其实都是这两种暗号的变奏。
先感受一下:两个开关怎么控制一盏灯
按位与 & 的规则是:两个对应位都是 1,结果才是 1。你可以把它想象成两个串联的开关——必须两个都闭合,灯泡才亮。
按位或 | 的规则是:只要有一个是 1,结果就是 1。这是两个并联的开关——任意一个闭合,灯泡就亮。
这个比喻很多地方都讲过。但我想让你注意一个更关键的点:这两个操作本质上是在做“筛选”和“合并”。
&是筛选:我想知道某个位是不是 1,就用一个只有那一位是 1 的掩码去“与”它。|是合并:我想把某个位强制变成 1,就用一个只有那一位是 1 的掩码去“或”它。
你看,一个在“问问题”,一个在“下命令”。这是两种完全不同的语义。
一道题检验你是不是真的懂了
来看这道题:
已知
int a = 0b1100, b = 0b1010;则表达式a & b的值是(二进制形式)?
逐位比较:
1 1 0 0
& 1 0 1 0
---------
1 0 0 0
结果是 0b1000。
这道题本身不难,但它暴露了一个非常典型的错误:把 & 和 | 搞反。如果你算出来是 0b1110,那就是错把“与”当成了“或”。很多人记规则的时候只记“有 0 则 0”或者“有 1 则 1”,但在紧张的考试或面试里,一慌就容易混。
我的建议是:不要背规则,去理解语义。& 是“同时满足”,| 是“至少满足一个”。你想象两个条件,一个要求“既……又……”,一个要求“或者……或者……”,就永远不会搞反。
位运算的真正威力:从一个区间异或说起
掌握了 & 和 | 之后,你其实已经拿到了打开位运算世界大门的钥匙。接下来我想借一道更有意思的题,让你看到这些基础操作是如何组合出优雅解法的。
题目是这样的:
定义
f(A, B)为A, A+1, ..., B这些数的按位异或值。给定A和B(最大到 10^12),求f(A, B)。
如果你没接触过这类问题,第一反应可能是:循环一遍不就完了?但 B 可以到 10^12,循环必然超时。
这时候我们需要一个关键洞察:异或运算满足自反性。也就是说,x ^ x = 0。这意味着,如果我们定义 g(n) = 0 ^ 1 ^ 2 ^ ... ^ n,那么 f(A, B) = g(B) ^ g(A-1)。因为从 A 到 B 的异或,等于前 B 个的异或再“抵消掉”前 A-1 个的异或。
那 g(n) 怎么快速算?这里有一个非常漂亮的规律——它和 n % 4 有关:
n % 4 == 0时,g(n) = nn % 4 == 1时,g(n) = 1n % 4 == 2时,g(n) = n + 1n % 4 == 3时,g(n) = 0
这个规律怎么来的?其实就是连续四个数的异或结果会归零,因为每四个数的最低位模式会完整循环一次。你可以自己验证几组,很快就能感受到这个节奏。
所以核心代码就几行:
long long g(long long n) {
if (n < 0) return 0;
if (n % 4 == 0) return n;
if (n % 4 == 1) return 1;
if (n % 4 == 2) return n + 1;
return 0;
}
long long f(long long A, long long B) {
return g(B) ^ g(A - 1);
}
你看,这里虽然主要用的是异或,但整个思路的根基还是位运算的思维:把一个大问题拆成位级别的规律。而 & 和 | 就是训练这种思维的第一步。
校验和:异或的另一个经典舞台
再看一个更贴近实际应用的场景:数据传输中的校验和。
将 N 个整数进行按位异或,得到的结果作为校验码。给定 N 个整数,计算它们的异或校验码。
这道题几乎就是异或的定义题。但我想让你注意它背后的思想:异或是一种“无损混合”。你把一堆数异或在一起,得到一个新的数。如果其中任何一个数变了,异或结果几乎一定会变。这就是为什么它适合做校验。
代码简单到不需要贴:
int checksum = 0;
for (int i = 0; i < n; i++) {
int x; cin >> x;
checksum ^= x;
}
但这里有一个容易被忽略的点:异或校验码的顺序无关。因为异或满足交换律和结合律,你无论按什么顺序异或,结果都一样。这也是它适合做校验的原因之一——发送方和接收方不需要约定顺序。
那些年我们踩过的坑
学到这里,你可能会觉得位运算不过如此。但真正写代码的时候,有几个坑几乎人人都会踩。
第一个坑:& 和 && 混用。
if (a & b) 和 if (a && b) 看起来很像,但语义完全不同。前者是按位与,结果是一个整数;后者是逻辑与,结果是布尔值。如果你本意是判断两个条件是否同时成立,却写了 &,编译器不会报错,但逻辑可能完全不对。
第二个坑:优先级。
if (x & y == 0) 实际上会被解释成 if (x & (y == 0)),因为 == 的优先级比 & 高。这种错误非常隐蔽,因为代码看起来“很合理”。正确的写法永远是加括号:if ((x & y) == 0)。
第三个坑:负数。
负数的二进制是补码,-1 的所有位都是 1。所以 -1 & 3 = 3,-1 | 3 = -1。初学者最好先用无符号整数或正整数练习,等熟悉了再处理负数。
从“开关”到“思维”
回到开头那个比喻:灯泡只有亮和灭,但通过不同的组合方式,我们可以表达出无穷的信息。& 和 | 就是最基本的两种组合方式——一个在问“是不是都满足”,一个在说“至少满足一个”。
当你真正理解了这两种语义,你会发现很多位运算技巧都变得自然:
- 用
n & 1判断奇偶,因为最低位决定了奇偶性。 - 用
n & (n - 1)消掉最低位的 1,因为减一之后最低位的 1 会变成 0,后面的 0 会变成 1,再与一下就把那个 1 消掉了。 - 用
n | (1 << k)把第 k 位设为 1,因为或运算的“至少一个为 1”特性。
这些技巧不是背出来的,而是从对 & 和 | 的深刻理解中“长”出来的。
如果你已经掌握了这些,下一步可以继续探索异或 ^ 的自反性、取反 ~ 的补码本质,以及左右移 << >> 的乘除语义。它们都是同一套思维的不同侧面。
位运算不难,难的是把它变成你的直觉。多写、多验证、多思考“为什么”,你很快就能在二进制世界里自由穿行。
关于作者
我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。
这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。
如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)