当前位置: 首页 > news >正文

位运算专题总结:从变量初始化陷阱到理解异或分组

位运算专题总结:从变量初始化陷阱到理解异或分组

在这里插入图片描述

目录

  • 刷题记录
  • 刷题过程
  • 位运算的核心概念
    • 基本位运算操作符
    • 常用位运算技巧
  • 我踩的坑总结
    • 坑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,其他位都是0
  • diff & 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日
相关专题: 哈希表、数学

http://www.dtcms.com/a/482708.html

相关文章:

  • Linux学习笔记(八)--环境变量与进程地址空间
  • 【动态规划】题目中的「0-1 背包」和「完全背包」的问题
  • Streamlit 中文全面教程:从入门到精通
  • 大模型系列-dify
  • 推荐系统:Python汽车推荐系统 数据分析 可视化 协同过滤推荐算法 汽车租赁 Django框架 大数据 计算机✅
  • 第16讲:深入理解指针(6)——sizeof vs strlen 与 指针笔试题深度解析
  • 【iOS】PrivacyInfo.xcprivacy隐私清单文件(二)
  • 环保网站建设公司排名手机访问wordpress网站卡
  • 从零构建大模型 Build a large language model from scratch by Sebastian Raschka 阅读笔记
  • 基于Chainlit和Llamalndex的智能RAG聊天机器人实现详解
  • 18.5 GLM-4大模型私有化部署实战:3秒响应+显存降低40%优化全攻略
  • Prisma 命令安全指南
  • Linux系统下文件操作系统调用详解
  • 网站备案后需要年检吗官方网站搭建
  • 515ppt网站建设北京朝阳区属于几环
  • 5~20.数学基础
  • HTML应用指南:利用POST请求获取全国鸿蒙智行门店位置信息
  • 优先级队列(堆)-295.数据流的中位数-力扣(LeetCode)
  • 大语言模型推理本质与技术演进
  • 福田区网站建最牛视频网站建设
  • 踩坑实录:Go 1.25.x 编译的 exe 在 Windows 提示“此应用无法运行”
  • 学习网站建设有前景没wordPress登不上数据库
  • 互联网大厂Java面试:从缓存技术到安全框架的深度探索
  • 本地部署开源集成工具 Jenkins 并实现外网访问( Linux 版本)
  • HackerNews 播客生成器
  • 新网站优化品牌营销策略四种类型
  • Linux 命令:umount
  • springboot159基于springboot框架开发的景区民宿预约系统的设计与实现
  • LatchUtils:简化Java异步任务同步的利器
  • 数据库设计基础知识(3)关系运算