引言
参照的是左程云的课程:https://space.bilibili.com/8888480/lists/3509640?type=series
本笔记包括了异或运算和位运算的高效技巧与应用,包括了class030 -> class031的内容
030【必备】异或运算的骚操作
异或运算的核心性质
前置知识
异或运算是计算机科学中一种重要的位运算,符号为^。在Python中实现位运算题目时需要特别注意溢出和符号扩展问题,通常需要手动处理:
1 | # Python中处理溢出的常见方法 |
异或运算的四大核心性质
1. 异或运算就是无进位相加
这是理解异或运算最重要的性质,其他所有性质都可以由此推导得出。
1 | 示例:5 ^ 3 |
/class30-%E5%BC%82%E6%88%96%E8%BF%90%E7%AE%97/%E5%BC%82%E6%88%96%E9%A2%84%E7%AE%97%E5%8F%96%E4%B8%8B%E6%A0%87.png)
2. 异或运算满足交换律和结合律
同一批数字,不管异或顺序如何,最终结果都相同:
- a ^ b = b ^ a(交换律)
- (a ^ b) ^ c = a ^ (b ^ c)(结合律)
3. 特殊值性质
1 | # 任何数与0异或等于自己 |
/class30-%E5%BC%82%E6%88%96%E8%BF%90%E7%AE%97/n%E6%88%96n.png)
4. 整体异或和性质
如果整体异或和为x,其中某部分异或和为y,那么剩余部分的异或和为x ^ y。
这个性质在很多题目中都有应用,特别是区间异或和相关的问题。
有趣的数学问题
让我们从一个有趣的概率问题开始:
问题:袋子里有a个白球,b个黑球。每次取2个球:
- 取出2个白球或2个黑球 → 放回1个白球
- 取出1白1黑 → 放回1个黑球
最终袋子里剩1个球,问这个球是黑球的概率?
答案:
- 如果黑球数量为偶数,最终是黑球的概率为0%
- 如果黑球数量为奇数,最终是黑球的概率为100%
- 完全与白球数量无关!
/class30-%E5%BC%82%E6%88%96%E8%BF%90%E7%AE%97/%E9%BB%91%E7%99%BD%E7%90%83%E7%BB%93%E6%9E%9C.png)
这个结果与异或运算的性质有关:黑球数量的奇偶性在整个过程中保持不变。
经典应用题目
题目1:用异或运算交换两数的值
1 | def swap(arr, i, j): |
原理分析:
- 第一步:
a = a ^ b,a存储了原始a和b的异或结果 - 第二步:
b = a ^ b ,则 b'=(a^b)^b - 第三步:
a = a ^ b ,则 a''=a^b^((a^b)^b)=a^b^a^b^b=a^b^a,根据交换律,a^b^a=a^a^b=b
题目2:不用判断语句返回两数最大值
1 | # 测试链接 : https://www.nowcoder.com/practice/d2707eaf98124f1e8f1d9c18ad487f76 |
核心思想:
- 通过位运算判断数字符号
- 用乘法实现条件选择,避免if语句
- 处理溢出情况,确保算法的鲁棒性
题目3:找到缺失的数字
题目:给定包含n个不同数字的数组,数字范围为[0,n],找出缺失的那个数字。
1 | # 测试链接 : https://leetcode.cn/problems/missing-number/ |
原理:
- 完整序列:0,1,2,…,n
- 给定数组:缺少一个数字
- 两者异或后,相同数字抵消,剩下的就是缺失数字
题目4:找到出现奇数次的数字
题目:数组中只有一种数出现奇数次,其他数都出现偶数次,找到这个数。
1 | # 测试链接 : https://leetcode.cn/problems/single-number/ |
题目5:找到两个出现奇数次的数字
题目:数组中有2种数出现奇数次,其他数都出现偶数次,返回这2种数。
1 | # https://leetcode.cn/problems/single-number-iii/ |
关键技巧:
n & -n:提取最右侧的1- 用这个位进行分组,将a和b分到不同组
- 每组内除了a或b,其他数都成对出现
Brian Kernighan算法原理:
1 | 假设 eor1 = 6 (二进制: 110) |
题目6:通用的k次方法
题目:数组中只有1种数出现次数少于m次,其他数都出现m次,找到这个数。
1 | # 测试链接 : https://leetcode.cn/problems/single-number-ii/ |
算法思路:
- 统计每一位上1的个数
- 如果某位上1的个数不是m的整数倍,说明目标数字在该位为1
- 重新构造答案
时间复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明度 |
|---|---|---|---|
| 交换两数 | O(1) | O(1) | 常数时间操作 |
| 找最大值 | O(1) | O(1) | 位运算替代条件判断 |
| 找缺失数字 | O(n) | O(1) | 遍历数组一次 |
| 找一个奇数次数字 | O(n) | O(1) | 异或运算线性时间 |
| 找两个奇数次数字 | O(n) | O(1) | 两次遍历,分组处理 |
| 通用k次方法 | O(n) | O(1) | 虽然有32层循环但32是常数 |
核心技巧总结
1. 位运算技巧
1 | # 提取最右侧的1 |
2. 异或运算应用模式
- 消除配对:相同数字异或为0,利用这个性质找到不配对的数字
- 分组策略:根据某一位的不同将数组分成两组
- 位统计:统计每一位上1的个数,重构答案
3. Python特有注意事项
1 | # 处理负数 |
异或运算虽然看起来简单,但在算法设计中有着广泛而巧妙的应用。掌握这些核心性质和应用模式,能够帮助我们解决很多看似困难的问题。特别是在处理数组中的配对、查找问题时,异或运算往往能提供O(1)空间复杂度的优雅解法。
031【必备】位运算的骚操作
前言
位运算有很多奇技淫巧,位运算的速度非常快,仅次于赋值操作,常数时间极好!
属于是大佬骚解,左神讲解,苯人copy就对了。
特别提醒:Python实现位运算的题目需要特别注意,需要自己去手动处理溢出和符号扩展等问题。
1 | # Python中处理溢出的常见方法 |
核心算法:Brian Kernighan算法
Brian Kernighan算法是位运算中的经典算法,用于提取二进制数中最右侧的1。
算法原理
1 | # 提取最右侧的1 |
工作原理:
n的二进制表示中,最右边的1右边可能有若干个0-n是n的补码表示(所有位取反后加1),所以可以进位到最右边的1,这个1左边的和n的左边也都是完全相反的n & -n只会保留最右边的1,其余位都变成0
示例演示:
1 | 假设 n = 12 (二进制: 1100) |
这个算法在很多高级位运算技巧中都有应用,是理解后续算法的基础。
经典应用题目
题目1:判断一个整数是不是2的幂
问题描述:给定一个整数n,判断它是否为2的幂次方。
1 | # 测试链接: https://leetcode.cn/problems/power-of-two/ |
算法原理:
- 2的幂的特点:二进制表示中只有一个1
- 例如:1(1), 2(10), 4(100), 8(1000), 16(10000)
- 利用Brian Kernighan算法提取最右侧的1
- 如果提取的结果等于原数,说明只有一个1
时间复杂度:O(1)
空间复杂度:O(1)
题目2:判断一个整数是不是3的幂
问题描述:给定一个整数n,判断它是否为3的幂次方。
1 | # 测试链接: https://leetcode.cn/problems/power-of-three/ |
算法原理:
- 利用数论知识:如果n是3的幂,那么n只含有质数因子3
- 1162261467 = 3^19,是32位整数范围内最大的3的幂
- 如果n也只含有质数因子3,那么1162261467一定能被n整除
- 这种方法引入了额外的先验知识,是一种巧妙的数学技巧
关键洞察:
- 3^19 = 1162261467 (32位int范围内最大的3的幂)
- 如果n是3的幂,则n只有质数因子3
- 因此最大的3的幂能被所有较小的3的幂整除
时间复杂度:O(1)
空间复杂度:O(1)
题目3:返回大于等于n的最小的2的幂
问题描述:给定一个非负整数n,返回大于等于n的最小的2的幂次方。
1 | def near2power(n): |
算法原理详解:
让我们以n = 100为例,详细演示算法过程:
Step 1:n = 100,先减1得到n = 99
1 | 99 的二进制:01100011 |
Step 2:逐步填充最高位右边的所有位
1 | n = 99 : 01100011 |
Step 3:n + 1 = 01111111 + 1 = 10000000 = 128
核心思想:
- 先减1:确保如果n本身是2的幂,结果仍然是n
- 逐步填充:将最高位1右边的所有位都填充为1
- 加1:得到下一个2的幂
为什么先减1?
- 如果n=16(10000),我们希望结果是16而不是32
- 减1后:15(01111)
- 填充后:15(01111)
- 加1后:16(10000) ✓
位运算技巧解析:
1 | # 通过连续的右移和或运算,将最高位的1向右"传播" |
时间复杂度:O(1) - 固定数量的位运算
空间复杂度:O(1) - 只使用常数额外空间
题目4:区间[left, right]内所有数字按位与的结果
问题描述:给定两个整数left和right,返回区间[left, right]内所有数字按位与的结果。
1 | # 测试链接: https://leetcode.cn/problems/bitwise-and-of-numbers-range/ |
算法原理:
按位与的关键特性:
- 如果区间[left, right]内某一位在这段区间内经历了从0到1的变化,那么最终结果这一位一定为0
- 只有left和right的公共前缀部分才可能保留为1
消去变化的位:
right & -right取出right的最右侧的1(最低位的1)- 每次把right的最右边的1消掉,right变小,靠近left
- 只要left < right,说明区间还有变化,继续消掉最低位的1
详细示例分析:
1 | 区间[5, 7]的按位与: |
算法执行过程:
1 | left=5(101), right=7(111) |
时间复杂度:O(log n) - 最多执行log(right)次循环
空间复杂度:O(1)
看题目5和题目6前的提醒
- 题目5和题目6代码看着跟脑子有大病一样,承认很强但似乎有点太嘚瑟了,是这样吗?
- 不是的,条件判断相比于赋值、位运算、算术运算是稍慢的,所以其实有现实意义
- 但是不需要追求在练算法过程中尽量少写条件判断,
- 那样会带来很多不必要的困扰,还是要写尽量直白、尤其是自己能理解的代码最好
- 大牛的实现欣赏完理解就好,下次当模版直接用
- 还是那句话:属于是大佬骚解,左神讲解,苯人copy就对了
题目5:反转二进制位(超自然版)
问题描述:将一个32位无符号整数的二进制位完全反转。
1 | # 测试链接: https://leetcode.cn/problems/reverse-bits/ |
算法原理深度解析:
这是著名的位分组逆序法(Bitwise reversal by mask),采用分治思想:
第1步:交换奇偶位
1 | # 0xaaaaaaaa = 10101010...10101010 (偶数位为1) |
第2步:交换每两位
1 | # 0xcccccccc = 11001100...11001100 (每两位的高位) |
第3步-5步:类似地交换4位、8位、16位
完整示例演示:
1 | 原数:11010110 (从左到右) |
为什么效率极高?
- 没有任何条件判断和循环
- 全部是位操作:按位与、或、移位
- 5次操作完成32位反转,而传统方法需要32次循环
时间复杂度:O(1) - 固定5次位运算
空间复杂度:O(1)
题目6:统计二进制中1的个数(超自然版)
问题描述:计算两个整数的汉明距离(二进制位不同的位置数目)。
1 | # 测试链接: https://leetcode.cn/problems/hamming-distance/ |
算法原理深度解析:
这是分组统计法,核心思想是逐步合并局部的1的个数:
详细示例(n = 13 = 1101):
初始状态:
1 | n = 1101 (二进制) |
第1步:每两位统计1的个数
1 | # 0x55555555 = 01010101...01010101 |
此时n的每2位表示该2位内1的个数:
- 10(二进制)= 2(十进制):前2位有2个1
- 01(二进制)= 1(十进制):后2位有1个1
第2步:每四位统计1的个数
1 | # 0x33333333 = 00110011...00110011 |
此时n=0011,表示整个4位中有3个1。
后续步骤类似,最终得到总的1的个数,更高位依此类推,每一步后,n的每2^k位表示这2^k位内1的总个数,最终全加到最低位
状态变化图解:
1 | 原始: 1101 (每位表示原始位值) |
为什么叫”分组统计”?
- 不是独立统计每组,而是累加合并
- 每步都使用上一步的统计结果
- 最终所有1的个数聚合到最低位
核心技巧总结:
- 利用掩码分离不同位置的位
- 用加法累积局部统计结果
- 分治思想:部分→整体
时间复杂度:O(1) - 固定5次位运算
空间复杂度:O(1)
算法性能对比与应用场景
性能对比表
| 算法 | 时间复杂度 | 空间复杂度 | 核心技巧 | 适用场景 |
|---|---|---|---|---|
| 判断2的幂 | O(1) | O(1) | Brian Kernighan算法 | 内存分配、哈希表大小 |
| 判断3的幂 | O(1) | O(1) | 数论+预计算 | 数学问题、特殊判断 |
| 最小2的幂 | O(1) | O(1) | 位填充技术 | 内存对齐、缓存优化 |
| 区间按位与 | O(log n) | O(1) | 公共前缀 | 区间查询、数据结构 |
| 反转二进制 | O(1) | O(1) | 分治+掩码 | 图像处理、编码转换 |
| 统计1的个数 | O(1) | O(1) | 分组统计 | 数据压缩、校验算法 |
实际应用场景
1. 系统编程中的应用
1 | # 内存对齐检查 |
2. 数据结构优化
1 | # 哈希表大小优化 |
3. 图像处理应用
1 | # 图像位操作 |
核心技巧模板总结
1. Brian Kernighan系列
1 | # 基础:提取最右侧的1 |
2. 位填充技术模板
1 | # 标准位填充(用于找下一个2的幂) |
3. 分治+掩码模板
1 | # 反转二进制位模板 |
4. Python位运算注意事项
1 | # 符号处理 |
扩展思考
1. 为什么位运算如此高效?
- CPU层面:位运算是最接近硬件的操作,执行速度极快
- 并行性:现代CPU可以并行处理多个位
- 无条件判断:避免了分支预测失误的性能损失
2. 什么时候不应该使用这些技巧?
- 代码可读性:团队协作时,清晰比技巧更重要
- 过度优化:在非性能关键路径上使用可能得不偿失
- 平台差异:某些技巧在不同架构上表现可能不同
3. 如何掌握位运算?
- 理解原理:每个技巧背后的数学/逻辑基础
- 动手实践:在纸上画出二进制变化过程
- 模板化:将常用技巧整理成模板
- 适度应用:在合适的场景使用,不要炫技
位运算虽然看起来神秘,但其本质是对二进制数据的高效操作。掌握了这些核心技巧后,不仅能解决特定的算法问题,更能在系统编程、性能优化等场景中发挥重要作用。关键是要在”技巧性”和”可读性”之间找到平衡,让代码既高效又易维护。