1. 题目解析与核心思路
这道LeetCode 201题要求我们计算一个整数区间内所有数字按位与的结果。乍看之下,最直观的解法可能是直接遍历区间内的所有数字进行按位与运算。但这种方法在区间较大时(比如left=1, right=2147483647)会非常低效,甚至导致超时。
1.1 按位与运算的特性
按位与(&)运算有一个重要特性:只要参与运算的数字在某一位上出现过0,那么最终结果的这一位就必定是0。这意味着:
- 如果区间内某个二进制位既有0又有1出现,该位结果必为0
- 只有那些在区间内所有数字都保持1的位,结果才会是1
举个例子,对于区间[5,7](二进制101到111):
- 最低位:5(1),6(0),7(1) → 有0出现 → 结果位0
- 中间位:5(0),6(1),7(1) → 有0出现 → 结果位0
- 最高位:5(1),6(1),7(1) → 全1 → 结果位1
最终结果为100(4)
1.2 关键观察:公共前缀
更深入的分析会发现,区间按位与的结果实际上就是左右端点的最长公共前缀。这是因为:
- 在从left到right的递增过程中,变化的位数必定经历了从0到1的变化
- 只有那些在left和right中保持不变的位才可能保持为1
- 这些不变的位就是它们的公共前缀部分
以[5,7]为例:
- 5: 101
- 7: 111
公共前缀是最高位的1,后面两位都发生了变化,所以结果就是100(4)
2. 算法实现与优化
2.1 方法一:右移找公共前缀
这个方法的思路很直观:通过不断右移left和right,直到它们相等,这样就找到了它们的公共前缀。然后我们再左移回去,补上移掉的0。
c复制int rangeBitwiseAnd(int left, int right) {
int shift = 0;
while (left < right) {
left >>= 1;
right >>= 1;
shift++;
}
return left << shift;
}
时间复杂度分析:
每次循环都将数字右移一位,最多需要移动log₂(n)次(n为数字的位数),所以时间复杂度是O(log n)。
空间复杂度:
只使用了常数个变量,空间复杂度是O(1)。
2.2 方法二:Brian Kernighan算法优化
这个方法利用了位运算的一个巧妙性质:对于任何数字n,n & (n-1)会将n的最低有效位1变为0。我们可以利用这个性质来快速缩小right的范围。
c复制int rangeBitwiseAnd(int left, int right) {
while (right > left) {
right &= (right - 1);
}
return right;
}
为什么这样有效:
每次right &= (right-1)操作都会去掉right的最低有效位1,这样right会逐渐减小,直到不大于left。此时right就是我们要找的公共前缀。
时间复杂度:
最坏情况下需要操作log₂(n)次(如left=1, right=2147483647),所以时间复杂度也是O(log n)。
两种方法对比:
- 方法一更直观,容易理解
- 方法二通常执行更快,因为不需要记录移位次数
- 在实际编码面试中,两种方法都可以接受,但最好能解释清楚原理
3. 边界条件与测试用例
3.1 常见边界情况
-
left == right:
- 直接返回left或right即可
- 例如[0,0]返回0,[5,5]返回5
-
left == 0:
- 无论right多大,结果都是0
- 因为0的二进制表示全0,按位与任何数都是0
-
right是最大整数(2³¹-1):
- 需要确保算法不会溢出
- 例如[1,2147483647]应该返回0
3.2 测试用例设计
好的测试用例应该覆盖以下场景:
- 一般情况(如[5,7])
- 单元素区间(如[3,3])
- 包含0的区间(如[0,1])
- 大区间(如[1,2147483647])
- 特殊位模式(如[0b1010,0b1100])
示例测试代码:
c复制void testCases() {
printf("%d\n", rangeBitwiseAnd(5, 7)); // 4
printf("%d\n", rangeBitwiseAnd(0, 0)); // 0
printf("%d\n", rangeBitwiseAnd(1, 2147483647)); // 0
printf("%d\n", rangeBitwiseAnd(10, 12)); // 8 (0b1000)
printf("%d\n", rangeBitwiseAnd(3, 3)); // 3
}
4. 深入理解与扩展应用
4.1 为什么公共前缀就是结果?
这个问题可以从二进制数的性质来理解。假设left和right的公共前缀是前m位,那么:
- 在公共前缀之后,left的后续位全0,right的后续位全1
- 在[left, right]区间内,必定存在两个数:
- 一个是公共前缀后面跟全0(即left本身)
- 一个是公共前缀后面跟全1(即right或某个中间数)
- 这两个数在非公共前缀的每一位上都有0和1出现
- 因此非公共前缀的所有位按位与后都是0
- 只有公共前缀部分保持不变
4.2 相关位运算技巧
这类问题在LeetCode中很常见,类似的位运算技巧还包括:
- 判断是否是2的幂:n & (n-1) == 0
- 计算汉明重量(1的个数):不断n &= n-1直到n=0
- 交换两个数:a ^= b; b ^= a; a ^= b;
- 找到只出现一次的数字:利用异或性质
4.3 实际应用场景
虽然这个问题看起来是纯理论的,但位运算在实际开发中有很多应用:
- 权限系统:用位掩码表示不同权限
- 网络编程:处理IP地址和子网掩码
- 图形处理:颜色值的位操作
- 嵌入式开发:直接操作硬件寄存器
5. 常见错误与调试技巧
5.1 新手常见错误
-
直接遍历计算:
c复制// 错误示例:大区间会超时 int result = left; for(int i=left+1; i<=right; i++) { result &= i; } return result; -
移位方向错误:
- 混淆了左移和右移的方向
- 忘记记录移位次数
-
边界条件处理不当:
- 没有考虑left=0的情况
- 没有处理left=right的情况
5.2 调试技巧
-
打印中间结果:
c复制while(left < right) { printf("left=%d, right=%d\n", left, right); left >>= 1; right >>= 1; shift++; } -
二进制可视化:
可以编写辅助函数打印数字的二进制表示:c复制void printBinary(int n) { for(int i=31; i>=0; i--) { printf("%d", (n>>i)&1); if(i%4==0) printf(" "); } printf("\n"); } -
使用调试器:
- 在关键位置设置断点
- 观察变量变化
- 单步执行验证逻辑
6. 性能优化与进阶思考
6.1 算法优化空间
虽然O(log n)的时间复杂度已经很优秀,但在极端情况下仍有优化空间:
-
使用内置指令:
- 某些CPU提供计算前导零的指令(如__builtin_clz)
- 可以利用这些指令快速找到不同的最高位
-
二分查找法:
- 可以尝试用二分法来寻找不同的最高位
- 虽然时间复杂度相同,但常数因子可能更小
6.2 相关题目推荐
掌握这个技巧后,可以尝试解决以下类似题目:
- LeetCode 137. 只出现一次的数字 II
- LeetCode 260. 只出现一次的数字 III
- LeetCode 338. 比特位计数
- LeetCode 393. UTF-8 验证
6.3 数学角度理解
从数学上看,这个问题可以表述为:
给定区间[left, right],求最大的m使得存在整数k满足:k×2ᵐ ≤ left ≤ right < (k+1)×2ᵐ
这个k×2ᵐ就是我们的按位与结果。这种表述揭示了问题背后的数学结构,可能启发其他解法。
7. 编码风格与工程实践
7.1 可读性优化
良好的编码风格可以提高代码的可维护性:
-
添加注释:
c复制// 计算区间[left, right]内所有数字的按位与 // 通过寻找left和right的公共前缀实现 int rangeBitwiseAnd(int left, int right) { ... } -
使用有意义的变量名:
- shift → shiftCount
- left → rangeStart
- right → rangeEnd
-
提取辅助函数:
c复制int findCommonPrefix(int a, int b) { int shift = 0; while(a < b) { a >>= 1; b >>= 1; shift++; } return a << shift; }
7.2 防御性编程
-
参数检查:
c复制if(left < 0 || right < 0 || left > right) { return -1; // 或抛出错误 } -
处理溢出:
- 虽然题目保证输入在[0,2³¹-1]范围内
- 但在实际工程中需要考虑边界情况
-
单元测试:
编写全面的测试用例验证各种边界条件
8. 面试技巧与准备建议
8.1 面试常见问题
面试官可能会问:
- 为什么这个方法有效?能证明吗?
- 时间/空间复杂度是多少?如何分析?
- 有没有其他解法?各有什么优缺点?
- 如何处理大输入?会溢出吗?
- 实际应用中有哪些场景会用到这种技巧?
8.2 回答策略
- 先解释暴力解法及其缺点
- 提出优化思路,解释关键观察(公共前缀)
- 逐步推导算法正确性
- 分析复杂度
- 讨论边界条件和测试用例
- 提及相关题目和实际应用
8.3 准备建议
- 熟练掌握位运算常见技巧
- 理解二进制数的性质
- 练习相关题目形成知识网络
- 模拟面试环境练习表达
- 总结常见问题模式
9. 个人实践心得
在实际解决这个问题时,我有几点深刻体会:
- 位运算问题往往有巧妙的解法,直接暴力通常不可行
- 观察二进制表示能提供关键洞察
- 公共前缀的概念在很多位运算问题中都很有用
- 测试边界条件非常重要,特别是0和最大整数值
- 两种解法各有优劣,理解原理比记忆代码更重要
一个实用的调试技巧是:当遇到难以理解的位运算时,把数字转换成二进制形式手动模拟运算过程,这往往能帮助发现规律。