从核心原理到算法应用全梳理
位运算直接对整数的二进制位执行操作,具备常数级时间、O(1)额外空间的天然优势,是算法优化、底层编程的核心工具。其中按位与(&)和异或(^)是最核心的两个运算符:& 擅长筛选、提取、算进位;^ 擅长抵消、对比、做还原。二者独立可解决基础题,组合可覆盖绝大多数进阶位运算题型。
一、按位与运算(&)
1. 核心原理
定义
对两个整数的二进制逐位对齐运算,规则:同一位上两个数都是 1,结果位才是 1;只要有一个是 0,结果位就是 0。
真值表
| 位A | 位B | A & B 结果 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
本质
按位与的核心是掩码筛选:构造一个只有目标位为1的「掩码」,和原数做与运算,即可精准保留目标位、清零其余所有位,实现对二进制位的精准控制。
核心性质
- 交换律:
a & b = b & a - 结合律:
(a & b) & c = a & (b & c) - 幂等律:
a & a = a(自身与自身运算结果不变) - 零律:
a & 0 = 0(和0运算,所有位全部清零)
2. 触发信号:什么时候想到用 &
题目出现以下特征时,优先考虑按位与运算:
- 需要判断二进制某一位是 0 还是 1
- 需要保留指定二进制位、清零其他所有位
- 需要计算二进制加法的进位值
- 判断奇偶、校验状态标志位
3. 经典应用场景
场景1:位状态判断(代表:判断整数奇偶)
为什么能这么做
构造一个「只有目标位是1、其余全是0」的掩码,和原数做按位与:
- 结果 ≠ 0 → 目标位为 1
- 结果 = 0 → 目标位为 0
代码示例
cpp
// 判断整数奇偶:最低位为1则是奇数
bool isOdd(int num) {
return (num & 1) == 1;
}
// 判断第k位是否为1
bool isBitSet(int num, int k) {
return (num & (1 << k)) != 0;
}
对应题目/场景
- 基础语法:奇偶判断、标志位校验
- 力扣461. 汉明距离:逐位统计1的个数
场景2:提取指定位,清零其余位(代表:提取最右侧的1)
为什么能这么做
掩码中为1的位置,与运算后保留原值;掩码中为0的位置,与运算后强制清零。 经典技巧 num & (~num + 1) 依托整数补码规则:对原数取反加1后,仅在原数最右侧1的位置为1,其余位全为0;和原数按位与后,最终只保留最右侧的1。
代码示例
cpp
// 提取num二进制最右侧的1,其余位清零
int getRightmostOne(int num) {
return num & (~num + 1);
}
对应题目/场景
- 力扣260. 只出现一次的数字 III:提取分组标记位
- 状态位提取、标志位筛选
场景3:计算二进制加法的进位
为什么能这么做
二进制加法中,只有 1 + 1 才会向高位产生进位,刚好匹配按位与「同1才1」的规则。进位需要进到高位,因此结果左移1位。
代码示例
cpp
// 计算a + b产生的进位总值
int getCarry(int a, int b) {
return (a & b) << 1;
}
对应题目/场景
- 力扣371. 两整数之和:不用加减号实现加法
二、异或运算(^)
1. 核心原理
定义
对两个整数的二进制逐位对齐运算,核心规则:相同为0,不同为1。
- 数学符号:
⊕ - 编程通用符号:
^(C、C++、Java、Python等语言通用)
真值表
| 输入A | 输入B | 结果 A⊕B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
本质:二进制无进位加法
异或等价于二进制逐位相加、丢弃所有进位,它和普通加法的本位计算结果完全一致,唯一区别是普通加法会向高位传递进位,而异或直接舍弃进位。
拓展:计算机底层的加法器正是由「异或(计算本位和)+ 与运算(计算进位)」组合实现,核心公式: a + b = (a \\oplus b) + ((a \& b) \<\< 1)
核心数学性质
所有算法技巧的底层根基,均可由定义直接推导得出:
- 交换律 :
a ^ b = b ^ a - 结合律 :
(a ^ b) ^ c = a ^ (b ^ c) - 自反性 :
a ^ a = 0(任何数与自身异或,结果为0) - 恒等性 :
a ^ 0 = a(任何数与0异或,数值保持不变) - 还原推论(对合性) :
a ^ b ^ b = a一个数连续异或同一个数两次,会还原为原始值。这是交换变量、加密解密、分组求解的核心原理。
2. 触发信号:什么时候想到用 ^
题目出现以下特征时,优先考虑异或运算:
- 元素成对出现、找出现奇数次的数字
- 找缺失的数字、找重复的数字
- 求两个数二进制位的差异数量
- 不使用临时变量交换两个整数
- 简单加密解密、数据还原
- 要求O(1)空间复杂度、线性时间复杂度
3. 经典应用场景
场景1:无需临时变量交换两个整数
为什么能这么做
依托异或的结合律与自反性,通过连续异或两次还原出对方的原始值,全程不需要第三个临时变量,空间复杂度O(1)。(但是这种方法存在的隐患是:当a,b是数组中的数时,要注意,a、b不能在同一个位置)
代码实现(Java)
cpp
// 初始状态:a = 甲, b = 乙
a = a ^ b; // 第一步:a = 甲^乙,b 仍为 乙
b = a ^ b; // 第二步:b = (甲^乙)^乙 = 甲,b完成交换
a = a ^ b; // 第三步:a = (甲^乙)^甲 = 乙,a完成交换
对应题目/场景
- 经典面试题:不使用临时变量交换两个整数
场景2:查找单个出现奇数次的数字
前置条件
数组中只有1个数字出现奇数次,其余所有数字均出现偶数次。
为什么能这么做
以0为初始值,遍历数组依次异或。出现偶数次的数字会两两异或、相互抵消为0,最终剩余的结果就是唯一出现奇数次的数字。
代码实现(Java)
public static void printOddTimesNum1(int[] arr) {
int eor = 0;
for (int cur : arr) {
eor ^= cur;
}
System.out.println(eor);
}
对应题目
- 力扣136. 只出现一次的数字
场景3:查找两个出现奇数次的数字
前置条件
数组中有2个不同的数字出现奇数次,其余所有数字均出现偶数次。
核心思路:分组异或
将问题拆解为两个「单个奇数次数字」的子问题:
- 先全员异或得到两个目标数的差值
eor = a ^ b - 用
eor & (~eor + 1)提取最右侧的1作为分组标记位(两个目标数在该位一定一个为0、一个为1) - 按标记位将数组分为两组,每组分别异或,各得到一个目标数
代码实现(Java)
public static void printOddTimesNum2(int[] arr) {
int eor = 0;
for (int i = 0; i < arr.length; i++) {
eor ^= arr[i];
}
// 提取二进制最右侧的1,作为分组依据
int rightOne = eor & (~eor + 1);
int onlyOne = 0;
for (int cur : arr) {
// 只异或该位为0的一组数
if ((cur & rightOne) == 0) {
onlyOne ^= cur;
}
}
// 输出两个出现奇数次的数字
System.out.println(onlyOne + " " + (eor ^ onlyOne));
}
位运算技巧说明:
eor & (~eor + 1)依托整数补码规则,对原数取反加1后,原数最右侧的1会被保留,其余位全部变为0;与原数按位与后,最终仅剩下最右侧的1。
对应题目
- 力扣260. 只出现一次的数字 III
场景4:标记二进制差异位
为什么能这么做
异或的结果天然就是两个数的「差异图」:每一个1都代表两个数在该位不同,每一个0都代表该位相同。可以直接把「找不同位的数量」转化为「统计结果中1的个数」。
代码示例
// 得到两个数的二进制差异标记图
int getDiff(int x, int y) {
return x ^ y;
}
对应题目
- 力扣461. 汉明距离
场景5:简易异或加密解密
为什么能这么做
利用异或的还原推论:加密和解密是完全相同的操作。明文异或密钥得到密文,密文再异或一次相同密钥,直接还原明文。
代码示例
// 加密解密为同一个操作
char xorCrypt(char c, int key) {
return (char)(c ^ key);
}
对应题目/场景
- 基础加密算法题、数据简单混淆
三、双运算符组合经典算法(进阶)
绝大多数位运算进阶题都是二者配合使用,以下三类是最经典的组合范式,明确每个运算符的分工即可快速解题。
组合1:汉明距离(力扣461)
触发信号
求两个整数二进制对应位不同的位置的数量。
分工原理
- 异或 ^:完成「差异编码」,把所有不同的位标记为1
- 按位与 &:完成「逐位校验」,统计结果中1的总个数
完整代码(C++)
class Solution {
public:
int hammingDistance(int x, int y) {
int diff = x ^ y;
int count = 0;
while (diff != 0) {
count += diff & 1;
diff = diff >> 1;
}
return count;
}
};
对应题目
- 力扣461. 汉明距离
- 力扣2220. 转换数字的最少位翻转次数
组合2:无加减号实现整数加法(力扣371)
触发信号
禁止使用 +、- 算术运算符,实现两个整数的加法。
分工原理
- 异或 ^:计算无进位的本位和
- 按位与 & + 左移:计算所有位产生的进位总值
- 循环迭代,直到进位为0,此时本位和就是最终结果
完整代码(C++)
class Solution {
public:
int getSum(int a, int b) {
while (b != 0) {
int carry = (unsigned int)(a & b) << 1; // 计算进位
a = a ^ b; // 计算无进位本位和
b = carry; // 进位作为新的b继续迭代
}
return a;
}
};
对应题目
- 力扣371. 两整数之和
组合3:两个出现奇数次的数字(力扣260)
触发信号
数组中恰好有两个数字只出现一次,其余所有数字都出现两次。
分工原理
- 异或 ^:全员异或得到两个目标数的差值
a ^ b - 按位与 &:提取差值最右侧的1作为分组掩码,同时用于判断每个数的分组
- 按分组分别异或,每组各得到一个目标数
完整代码(C++)
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int eor = 0;
for (int num : nums) {
eor ^= num;
}
// 提取最右侧的1作为分组依据
int rightOne = eor & (~eor + 1);
int onlyOne = 0;
for (int num : nums) {
if ((num & rightOne) == 0) {
onlyOne ^= num;
}
}
return {onlyOne, eor ^ onlyOne};
}
};
对应题目
- 力扣260. 只出现一次的数字 III
四、总结与解题方法论
1. 核心逻辑总结
所有异或的编程技巧,本质都是对「自反性 + 还原推论」的复用:
- 单个奇数次数字:全员异或,偶数次数字全部抵消
- 两个奇数次数字:用差异位拆分,退化为两个单次问题
- 交换变量:连续异或两次,还原出对方原值
按位与则始终扮演「精准控制比特位」的角色:判断、提取、算进位,和异或形成互补。
这类位运算方案的统一优势:时间复杂度O(n),空间复杂度O(1),无需额外的哈希表、数组等存储结构,是算法优化中的经典手段。
2. 固定解题思考步骤
拿到一道位运算题,按以下四步走即可:
- 看特征匹配:看题目关键词,匹配「触发信号」,确定用&、^还是组合使用
- 明确分工:想清楚每个运算分别做什么:谁做抵消、谁做标记、谁做筛选
- 小样例推演:用小数手动计算一遍二进制,验证逻辑是否正确
- 编码实现:翻译成代码,注意符号位、边界条件