交换变量、加密、找孤数字:Python 里这个小符号比想象中更强大
如果我告诉你,有一个运算规则能让数字“消失”——5 ^ 5 的结果是 0,而且没有引入任何新的数字,你会不会觉得有点反常识?更奇怪的是,在 Python 里 ~5 的结果不是 -5,而是 -6。这两个运算符号 ^ 和 ~,在很多初学者看来只是冷门的语法糖,但在懂行的人手里,它们是处理二进制信息的利器——用来交换变量、加密数据、甚至解出博弈论题目。
先说异或 ^。它的规则一句话就能概括:相同为 0,不同为 1。把两个数转换成二进制,逐位对照:两位一样就写 0,不一样就写 1。
1 0 1 (5)
^ 0 1 1 (3)
--------
1 1 0 (6)
这个规则像“找不同”游戏,但更妙的是它有两个几乎违反直觉的性质:
x ^ x = 0:一个数和自己异或,每一位都相同,结果全部归零。x ^ 0 = x:任何数和 0 异或,0 的那一位是 0,和它不同就保留原位的值,相同就变 0——结果恰好是它本身。
这两个性质组合出了一个经典技巧:不借助临时变量交换两个整数。
a = 5
b = 3
a = a ^ b # a = 6 (5^3)
b = a ^ b # b = 5 (6^3, 因为 6^3=5, 它其实是原来的 a)
a = a ^ b # a = 3 (6^5, 它其实是原来的 b)
print(a, b) # 3 5
背后的逻辑不难:a ^ b 把两个数的“差异”存进了 a,然后这个差异分别和原数异或,就能还原出另一个数。每一步都走得很稳,没有多余的内存。
理解这一步之后,再看一个经典编程题:在一个整数数组里,只有一个数出现了奇数次,其余数都出现偶数次,找出这个数。暴力写一个哈希表也能做,但用异或只需一行核心逻辑:
result = 0
for num in nums:
result ^= num
因为 x ^ x = 0,所有成对出现的数都会互相抵消,最终剩下的就是那个“落单”的数。这是异或性质最纯粹、最漂亮的一次展示——它甚至不需要额外的空间。作为对比,哈希表需要 O(n) 的空间,而异或解法是 O(1)。
异或还有一个极其实用的价值:加密。加密就是让信息变得面目全非,解密就是把面目全非的信息还原。encrypted = secret ^ key,然后 decrypted = encrypted ^ key,因为异或的自反性,key 用两次就绕回来了。虽然这不是真正的安全加密,但用来理解位操作的本质非常直观。
讲完 ^,轮到取反 ~。规则也简单:0 变 1,1 变 0。但 Python 里 ~5 却得到 -6,这就让很多人一头雾水了。
~5 # 结果是 -6,不是 -5
原因藏在 Python 整数的存储方式——补码。对于正数 5,它的二进制可以想象成 ...000101,取反后变成 ...111010,而在补码表示中,这是一个负数的存储形式,对应的真值是 -6。数学上可以简化为:~x = -x - 1。
很多初学者会踩两个坑:第一,认为 ~5 等于 -5;第二,认为 ~0 等于 0。实际上 ~0 = -1。这个运算并不负责“变号”,它只是把所有位翻转了,负号是补码系统带来的副产品。
学到这里,我们不妨把视线抬高一寸,看看位运算在真实比赛题目里到底怎么考。
有一类经典的博弈题叫“高僧斗法”,描述了一群小和尚站在台阶上,两个法师轮流指挥一个小和尚向上移动任意级台阶,但又不能越过前面的人,最后无法移动者输。这听起来完全是个模拟题,跟位运算没关系。但如果把问题抽象成尼姆博弈,每一对相邻小和尚之间的距离差,就构成了一个尼姆堆。而判断先手能否必胜、如何走第一步,核心操作就是求所有堆的异或值。
如果你已知尼姆博弈的结论——各堆异或和为 0 时先手必败,否则先手必胜——那么这道题就转化成了:在合法移动范围内,找到一种操作使得新的异或和为 0。如果不理解异或,这道题根本无从下手;理解了异或,便懂得“异或和”表示的是所有堆的一个平衡状态,而移动一个堆就是在改变这个平衡。
再看另一道题:给定 N 个数和一个上限 K,在 0 到 K 之间选一个 X,使得 f(X) = (X ^ A[1]) + (X ^ A[2]) + ... + (X ^ A[N]) 最大。这类题的突破口是按位贪心:从高位到低位逐位决定 X 的二进制位。每一位对总和的贡献,取决于所有 A 在这一位上有多少个 0、多少个 1。如果 X 的这一位取 1,那么就能让所有 A 中该位为 0 的数字在这一位获得收益;取 0 则相反。所以每决定一位之前,统计一下 0 的个数和 1 的个数就够了。异或在这里的作用不是说“异或结果等于多少”,而是帮助你把“每一位的收益”独立拆开来分析——正是因为异或不进位、不跨位,才能按位贪心。这种“位独立性”恰恰是异或和加法最大的区别,也是最常被忽略的特性。
回到 ~。虽然它的名气不如 ^,但在位掩码操作中经常配合 & 使用。比如想清空某个整数的第 3 位,可以先 ~ (1 << 3) 得到一个只有第 3 位是 0、其余位全是 1 的掩码,再与该整数做 & 运算。这也是取反最常见的工程用途——不是拿来求负数,而是用来构造掩码。
现在回头看这两个运算符,你会发现它们其实代表两种截然不同的思维模式。^ 关心的是“不同”,它是求差异、求对称差的工具;~ 关心的是“翻转”,它是从补码角度理解负数的入口。前者在做消除、对比、加密,后者在做取反、构造掩码。二者都建立在同一个事实之上:你在和二进制位打交道,而不是和十进制数字打交道。
如果你真想熟练掌握位运算,我建议多做三类练习:一是把十进制数翻译成二进制,手算异或和取反,直到形成条件反射;二是用 ^ 解决数组找重、字符串找差异这类经典问题;三是做几道需要“按位贪心”的题目,感受一下位运算如何与贪心、博弈等算法思想结合在一起。当你能在读完题的十秒内,想到“这题可以用异或”或者“这题需要逐位处理”时,你就真正跨过了理解位运算的门槛。
关于作者
我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。
这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。
如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)