位运算专题总结:从变量初始化陷阱到理解异或分组
位运算专题总结:从变量初始化陷阱到理解异或分组
目录
- 刷题记录
- 刷题过程
- 位运算的核心概念
- 基本位运算操作符
- 常用位运算技巧
- 我踩的坑总结
- 坑1:数组溢出
- 坑2:位运算没赋值
- 坑3:修改循环变量
- 坑4:整型溢出
- 坑5:变量初始化陷阱
- 坑6:索引vs值混淆
- 坑7:循环范围错误
- 坑8:位判断条件错误
- 典型题目分类
- 类型1:基础位操作
- 类型2:异或消消乐
- 类型3:位图思想
- 类型4:位计数法
- 类型5:异或分组
- 位运算常用技巧总结
- 我的理解
刷题记录
- 刷题周期: 2天(10.13 - 10.14)
- 完成题量: 10题全部AC
- 题目分布: 基础5题 + 进阶5题
刷题过程
Day12(10.13): 位运算基础(5题)
- 位1的个数、比特位计数、汉明距离、只出现一次的数字、只出现一次的数字III
- 掌握基本位操作:
n & (n-1)
清除最低位的1、异或的"消消乐"性质 - 第1题踩坑:数组溢出 + 右移没赋值
- 第2题踩坑:修改了循环变量导致死循环
- 第5题踩坑:数组越界 + 整数溢出(用unsigned int解决)
Day13(10.14): 位运算进阶(5题)
- 判定字符是否唯一、丢失的数字、两整数之和、只出现一次的数字II、消失的两个数字
- 核心技巧:位图思想、异或分组、位计数法、位运算实现加法
- 第1题踩坑:"索引vs值"混淆、
for(auto ch : bitMap)
遍历对象错误 - 第2题踩坑:
int a, b = 0
只初始化了b - 第5题踩坑:循环范围
[0, n)
vs[1, n]
搞错、分组判断条件== 1
vs!= 0
难度递进:
- 基础:位操作、异或消消乐
- 进阶:位图、异或分组、位计数法
- 综合应用:结合多个技巧解决复杂问题
位运算的核心概念(我的理解)
什么是位运算?
我的理解就是:直接对二进制位进行操作,通过位与、位或、异或、左移、右移等运算来解决问题。
关键是:
- 各种位运算符的含义
- 什么时候用位运算?
- 位运算有哪些经典技巧?
基本位运算符
1. 左移 <<
: 相当于乘以2的幂次
n << k = n * 2^k
3 << 2 = 3 * 4 = 12
2. 右移 >>
: 相当于除以2的幂次
n >> k = n / 2^k
12 >> 2 = 12 / 4 = 3
3. 按位与 &
: 两位都为1才为1
n & 1 // 检查最低位是否为1
n & (n-1) // 清除最低位的1(经典技巧)
n & (-n) // 获取最低位的1
4. 按位或 |
: 有1则为1
bitMap |= (1 << i) // 设置第i位为1
5. 按位异或 ^
: 相同为0,不同为1
a ^ a = 0 // 自己异或自己等于0
a ^ 0 = a // 和0异或等于自己
a ^ b ^ a = b // 消消乐性质
位运算的五种经典应用
通过这10道题,我发现位运算主要有5种应用:
1. 位图思想(用int的位表示状态)
核心: 用一个int的32位来代替数组,节省空间
应用: 判断字符是否唯一
关键操作:
int bitMap = 0;
int i = ch - 'a'; // 字符转索引// 检查第i位
if(((bitMap >> i) & 1) == 1) {// 该位是1
}// 设置第i位为1
bitMap |= (1 << i);
我做过的题:
- 面试题01.01 - 判定字符是否唯一
2. 异或消消乐(找唯一元素)
核心: a ^ a = 0
, a ^ 0 = a
,相同的数异或后消失
应用: 找数组中只出现一次的数
模板:
int ret = 0;
for(int num : nums) {ret ^= num;
}
return ret; // 其他数都消掉了,剩下只出现一次的
我做过的题:
- LeetCode 136 - 只出现一次的数字
- LeetCode 268 - 丢失的数字
3. 异或分组(找两个唯一元素)
核心: 先异或所有数得到 a ^ b
,再找分界位分组
步骤:
// 1. 异或所有数,得到a^b
int xor_all = 0;
for(int num : nums) xor_all ^= num;// 2. 找分界位(最低位的1)
unsigned int diff = xor_all & (-xor_all);// 3. 按分界位分组异或
int a = 0, b = 0;
for(int num : nums) {if((diff & num) != 0) { // 注意:!= 0a ^= num;} else {b ^= num;}
}
return {a, b};
我做过的题:
- LeetCode 260 - 只出现一次的数字III
- 面试题17.19 - 消失的两个数字
4. 位计数法(统计每位的1)
核心: 统计所有数在每一位上1的个数,% k
得到唯一数的该位
应用: 其他数出现k次,找只出现1次的
模板:
int ret = 0;
for(int i = 0; i < 32; i++) { // 遍历32位int sum = 0;for(int num : nums) {if(((num >> i) & 1) == 1) {sum++;}}sum %= k; // 对k取模if(sum == 1) {ret |= (1 << i); // 设置第i位}
}
return ret;
我做过的题:
- LeetCode 137 - 只出现一次的数字II
5. 位运算实现加法
核心: 异或=无进位加法,与+左移=进位
模板:
while(b != 0) { // b是进位int x = a ^ b; // 无进位加法unsigned int carry = (unsigned int)(a & b) << 1; // 进位a = x;b = carry;
}
return a;
我做过的题:
- LeetCode 371 - 两整数之和
典型题目分类
类型1:位图思想
面试题01.01. 判定字符是否唯一
我的第一次错误:又是索引vs值!
int hash[26];
for(int i = 0; i < n; i++) {hash[astr[i]]++; // ❌ astr[i]是字符,ASCII值是97+
}
问题: astr[i]
是字符,比如’a’的ASCII值是97,但数组只有26个位置(0-25)!
正确写法:
hash[astr[i] - 'a']++; // ✅ 字符转索引
这是我第N次犯"索引vs值"错误了!
位图方法(真正的解法):
int bitMap = 0;
for(auto ch : astr) {int i = ch - 'a';// 检查第i位是否为1if(((bitMap >> i) & 1) == 1) {return false; // 出现过}// 设置第i位为1bitMap |= (1 << i);
}
return true;
核心技巧:
bitMap >> i
:把第i位移到最低位& 1
:只保留最低位1 << i
:生成只有第i位为1的数|=
:按位或,设置该位为1
优势: 用1个int(4字节)代替26个int(104字节)!
我的第二次错误:
for(auto ch : bitMap) { // ❌ int不能遍历!// ...
}
问题: bitMap
是int,不是容器,不能用范围for!
正确写法:
for(auto ch : astr) { // ✅ 遍历字符串// ...
}
我的第三次错误:
bitMap |= (i << 1); // ❌ i左移1位
问题: 应该是"1左移i位",不是"i左移1位"!
正确写法:
bitMap |= (1 << i); // ✅ 1左移i位
举例:
设置第5位:
✅ 1 << 5 = 00100000 = 32
❌ 5 << 1 = 00001010 = 10
类型2:异或消消乐
LeetCode 268. 丢失的数字
思路: 把数组中的数和完整序列的数都异或起来,相同的数会消掉,剩下缺失的数。
我的第一次错误:变量初始化陷阱!
int ret1, ret2 = 0; // ❌ 只初始化了ret2!
问题分析:
int ret1, ret2 = 0;
// 等价于:
int ret1; // 未初始化,随机值!
int ret2 = 0; // 初始化为0
测试:
输入:[3, 0, 1]
输出:-262537124 (ret1的随机值)
预期:2
正确写法:
int ret1 = 0, ret2 = 0; // ✅ 都要初始化
// 或者:
int ret = 0; // 只用一个变量更好
for(auto x : nums) ret ^= x;
for(int i = 0; i <= nums.size(); i++) ret ^= i;
return ret;
教训:C++的变量初始化陷阱!int a, b = 0;
只初始化b!
LeetCode 136. 只出现一次的数字
我的第一次错误:
int ret = 0;
for(int i = 0; i < nums.size(); i++) {ret = nums[i] ^ nums[i+1]; // ❌ 每次都覆盖ret
}
问题: 应该是累积异或,不是每次覆盖!
正确写法:
int ret = 0;
for(int num : nums) {ret ^= num; // ✅ 累积异或
}
return ret;
类型3:异或分组
LeetCode 260. 只出现一次的数字III
思路: 找两个只出现一次的数,需要分组异或。
我的第一次错误:
for(int num : nums) {xor_all ^= nums[num]; // ❌ 用元素值当索引
}
问题: num
已经是元素值了,不需要再 nums[num]
!
正确写法:
for(int num : nums) {xor_all ^= num; // ✅ 直接异或元素值
}
我的第二次错误:整数溢出!
int diff = xor_all & (-xor_all); // ❌ 当xor_all=INT_MIN时溢出
问题: -INT_MIN
溢出了!
正确写法:
unsigned int xor_all = 0; // ✅ 用unsigned int
unsigned int diff = xor_all & (-xor_all);
为什么unsigned能解决?
INT_MIN = -2147483648
-INT_MIN溢出了(超过INT_MAX)unsigned int 没有负数,-x会自动转成补码,不会溢出
面试题17.19. 消失的两个数字
这题是LeetCode 268 + 260的综合!
我的第一次错误:循环范围错了!
for(int i = 0; i < nums.size() + 2; i++) { // ❌ 从0开始xor_ret ^= i;
}
问题: 题目说的是 [1, N]
,不是 [0, N-1]
!
正确写法:
for(int i = 1; i <= nums.size() + 2; i++) { // ✅ 从1开始xor_ret ^= i;
}
我的第二次错误:分组判断条件错了!
if((diff & num) == 1) { // ❌ diff不一定是1a ^= num;
}
问题: diff = xor_all & (-xor_all)
得到的是最低位的1,可能是1, 2, 4, 8…
举例:
xor_all = 6 (110)
diff = 2 (010) // 不是1!num = 3 (011)
diff & num = 010 & 011 = 010 = 22 != 1,但应该判断为同一组!
正确写法:
if((diff & num) != 0) { // ✅ 判断是否不为0a ^= num;
}
原理:
diff
只有一位是1,其他位都是0diff & num
的结果要么是0(该位为0),要么是diff(该位为1)- 所以应该判断
!= 0
或== diff
类型4:位计数法
LeetCode 137. 只出现一次的数字II
题目: 其他数出现3次,找只出现1次的
为什么不能用异或?
因为异或只能处理"出现2次"的情况(a ^ a = 0
),出现3次就不行了。
解法:位计数法
int ret = 0;
for(int i = 0; i < 32; i++) { // 遍历32位int sum = 0;for(int j = 0; j < nums.size(); j++) {if(((nums[j] >> i) & 1) == 1) {sum++; // 统计第i位上1的个数}}sum %= 3; // 对3取模if(sum == 1) {ret |= (1 << i); // 该位是1}
}
return ret;
核心思想:
nums = [2, 2, 3, 2]
二进制:
2: 010
2: 010
3: 011 ← 目标数
2: 010第0位(最右):0+0+1+0=1 → 1%3=1 → ret第0位是1 ✅
第1位:1+1+1+1=4 → 4%3=1 → ret第1位是1 ✅
第2位:0+0+0+0=0 → 0%3=0 → ret第2位是0 ✅结果:ret = 011 = 3 ✅
时间复杂度: O(32n) = O(n)
LeetCode 191. 位1的个数
我的第一次错误:
int a[INT_MAX]; // ❌ 栈溢出!
问题: INT_MAX太大了,数组放不下!而且根本不需要数组。
我的第二次错误:
while(n > 0) {i >> 1; // ❌ 没有赋值,死循环!
}
问题: i >> 1
只是计算,没有赋值!
正确写法:
while(n > 0) {if(n & 1) ret++;n >>= 1; // ✅ 赋值
}
更优雅的方法:n & (n-1)
技巧
while(n) {n &= (n-1); // 每次清除最低位的1ret++;
}
为什么 n & (n-1)
能清除最低位的1?
n = 12 (1100)
n-1 = 11 (1011)
n & (n-1) = 1100 & 1011 = 1000 (清除了最右边的1)
LeetCode 338. 比特位计数
我的第一次错误:
for(int i = 0; i <= n; i++) {while(i > 0) { // ❌ 修改了循环变量iif(i & 1) tmp++;i >>= 1;}ans[i] = tmp;
}
问题: 在for循环里修改了循环变量i,导致死循环!
正确写法:
for(int i = 0; i <= n; i++) {int tmp = i; // ✅ 用临时变量while(tmp > 0) {if(tmp & 1) count++;tmp >>= 1;}ans[i] = count;
}
更优雅的方法:DP
for(int i = 0; i <= n; i++) {ans[i] = ans[i >> 1] + (i & 1);
}
原理:
i = 6 (110)
i >> 1 = 3 (011)6的1的个数 = 3的1的个数 + 6最低位(0)
类型5:位运算实现加法
LeetCode 371. 两整数之和
题目: 不用+和-实现加法
核心思路:
- 异或
^
= 无进位加法 - 与+左移
(a & b) << 1
= 进位
代码:
while(b != 0) { // b是进位int x = a ^ b; // 无进位加法unsigned int carry = (unsigned int)(a & b) << 1; // 进位a = x;b = carry;
}
return a;
举例:
a = 5 (101), b = 3 (011)第1轮:x = 101 ^ 011 = 110 (无进位)carry = (101 & 011) << 1 = 001 << 1 = 010 (进位)a = 110, b = 010第2轮:x = 110 ^ 010 = 100carry = (110 & 010) << 1 = 010 << 1 = 100a = 100, b = 100第3轮:x = 100 ^ 100 = 000carry = (100 & 100) << 1 = 100 << 1 = 1000a = 000, b = 1000第4轮:x = 000 ^ 1000 = 1000carry = (000 & 1000) << 1 = 000 << 1 = 0a = 1000, b = 0b=0,退出,返回1000 (8) ✅
为什么要用unsigned int?
因为负数左移是未定义行为:
a = -1, b = -2
a & b = 负数(a & b) << 1 // ❌ 负数左移,未定义行为(unsigned int)(a & b) << 1 // ✅ 转成unsigned再左移
我踩的坑(总结)
坑1:变量初始化陷阱
// ❌ 错误
int a, b = 0; // 只有b被初始化// ✅ 正确
int a = 0, b = 0; // 都初始化
坑2:位运算的操作对象搞错
// ❌ 错误
bitMap |= (i << 1); // i左移1位// ✅ 正确
bitMap |= (1 << i); // 1左移i位
坑3:字符转索引
// ❌ 错误
hash[ch]++; // ch是字符,ASCII值97+// ✅ 正确
hash[ch - 'a']++; // 转成0-25
坑4:循环变量vs元素值
// ❌ 错误
for(int i : nums) {xor_all ^= nums[i]; // i是元素值,不是索引
}// ✅ 正确
for(int num : nums) {xor_all ^= num;
}
坑5:分组判断条件
// ❌ 错误
if((diff & num) == 1) // diff不一定是1// ✅ 正确
if((diff & num) != 0) // 判断是否不为0
坑6:题目范围理解错误
// ❌ 错误:[1, N] 从0开始
for(int i = 0; i < n; i++)// ✅ 正确
for(int i = 1; i <= n; i++)
坑7:负数左移
// ❌ 错误
int carry = (a & b) << 1; // 负数左移,未定义// ✅ 正确
unsigned int carry = (unsigned int)(a & b) << 1;
坑8:整数溢出
// ❌ 错误
int diff = xor_all & (-xor_all); // INT_MIN溢出// ✅ 正确
unsigned int xor_all = 0;
unsigned int diff = xor_all & (-xor_all);
坑9:修改循环变量
// ❌ 错误
for(int i = 0; i <= n; i++) {while(i > 0) { // 修改了ii >>= 1;}
}// ✅ 正确
for(int i = 0; i <= n; i++) {int tmp = i; // 用临时变量while(tmp > 0) {tmp >>= 1;}
}
坑10:遍历对象搞错
// ❌ 错误
for(auto ch : bitMap) // int不能遍历// ✅ 正确
for(auto ch : astr) // 遍历字符串
我的薄弱环节
- C++基础语法:变量初始化、负数左移等陷阱
- 位运算技巧:
n & (n-1)
等技巧要多练 - 题目理解:范围
[0, n]
vs[1, n]
要仔细看
下一步计划
- 位运算基础已经掌握
- 异或分组技巧要再练几题
- 位计数法理解了,可以应对"出现k次"的问题
典型模板总结
位图模板
int bitMap = 0;
for(auto ch : str) {int i = ch - 'a';// 检查第i位if(((bitMap >> i) & 1) == 1) {// 该位是1}// 设置第i位为1bitMap |= (1 << i);
}
异或消消乐模板
int ret = 0;
for(int num : nums) {ret ^= num;
}
return ret;
异或分组模板
// 1. 异或所有数
int xor_all = 0;
for(int num : nums) xor_all ^= num;// 2. 找分界位
unsigned int diff = xor_all & (-xor_all);// 3. 分组异或
int a = 0, b = 0;
for(int num : nums) {if((diff & num) != 0) { // 注意:!= 0a ^= num;} else {b ^= num;}
}
return {a, b};
位计数模板
int ret = 0;
for(int i = 0; i < 32; i++) {int sum = 0;for(int num : nums) {if(((num >> i) & 1) == 1) {sum++;}}sum %= k; // k是其他数出现的次数if(sum == 1) {ret |= (1 << i);}
}
return ret;
位运算加法模板
while(b != 0) {int x = a ^ b; // 无进位加法unsigned int carry = (unsigned int)(a & b) << 1; // 进位a = x;b = carry;
}
return a;
我的理解
位运算的本质:
- 直接操作二进制位,效率高
- 很多问题可以用位运算优化空间或时间
异或的强大:
- 消消乐性质:找唯一元素
- 交换律和结合律:可以任意调换顺序
- 分组技巧:找两个唯一元素
位计数法的通用性:
- 当异或不适用时(出现k次,k≠2)
- 统计每位1的个数,
% k
得到结果 - 时间复杂度:O(32n) = O(n)
位运算 vs 普通方法:
问题 | 普通方法 | 位运算方法 |
---|---|---|
判断字符唯一 | hash数组104字节 | int变量4字节 |
找唯一元素 | 哈希表O(n)空间 | 异或O(1)空间 |
实现加法 | 用+运算符 | 异或+与运算 |
什么时候复习:
- 异或分组技巧要再做几题
- 位计数法理解了,暂时不用复习
- 位运算实现加法很有趣,可以再研究
总结于: 2025年10月14日
相关专题: 哈希表、数学