题目链接:Bitwise AND of Numbers Rangeleetcode
题目与直觉理解
题目:给定两个整数 left 和 right,表示闭区间 [left, right],返回区间内所有整数的按位与结果。leetcode
约束:0 <= left <= right <= 2^31 - 1,也就是 32 位非负整数范围。leetcode
直觉上,如果区间很大(比如从 1 到一个非常大的数),那么某些二进制位在中间某个数上一定会变成 0,最后按位与结果也会在这些位上变成 0。唯一可能保持为 1 的,只能是整个区间里从头到尾都从未变化过的那部分"高位前缀"。
初始错误思路:只看"最高位 1"
一开始容易想到的一个思路是:
把每个数都看成 32 位整数,找"最高位的 1 在哪一位"。
如果区间内所有数的最高位 1 都在同一位,就认为结果是"只有这一位是 1,其余位为 0";否则返回 0。
这个思路抓住了"高位"这个感觉,但有两个致命问题:
只看"最高位是否一致",忽略了"更低位可能也一直为 1"。例如区间 [6, 7]:
6 = 110,7 = 111,最高位 1 都在同一位,但真实结果是 110 (6),而不是 100 (4)。
没有真正找到"公共前缀"的全部部分,等于只保留了最左边那一位。
这说明:我们需要的不是"最高位",而是 left 和 right 的完整"二进制公共前缀"。
正确核心:找二进制公共前缀
这道题有一个非常典型的结论:
区间 [left, right] 所有数的按位与结果,就是 left 和 right 在二进制下的 公共前缀,其余低位全部变为 0。leetcode
原因可以这样理解:
从 left 增加到 right 的过程中,只要某一位在这个过程中经历过从 0 变 1 或从 1 变 0 的变化,那么这一位在某个数上必然是 0,按位与之后这个位就会变 0。
只有在 left 和 right 对应位完全一致、且在整个区间内不发生变化的那一段"高位前缀",才能在结果中保持为 1。
所以问题变成:如何高效地求出 left 和 right 的公共前缀?
经典解法:不断右移,直到相等
一种常见且优雅的做法是:
用一个计数 shift = 0 记录右移的次数。
当 left < right 时,同时右移 left 和 right 一位(left >>= 1; right >>= 1;),并让 shift++。
循环直到 left == right。此时的 left(或 right)就是它们的公共前缀。
最后将这个公共前缀左移 shift 位,把低位补回 0:left << shift,即为答案。leetcode
示意伪代码(C 风格):
cpp
int rangeBitwiseAnd(int left, int right) {
int shift = 0;
while (left < right) {
left >>= 1;
right >>= 1;
++shift;
}
return left << shift;
}
这里用 while (left < right) 和 while (left != right) 在题目保证 left <= right 的前提下是等价的,选择哪个更多是风格问题。
为什么右移有效:从"丢掉一定为 0 的尾部"看
每次右移一位,在本质上是在做:丢掉当前的最低位。
当某一位开始在 left 与 right 之间发生差异时,说明在区间 [left, right] 内,这一位必然经历了 0/1 的变化。
因为按位与对所有数都要是 1 才能为 1,一旦某一位在区间里出现过 0,最终结果这位一定是 0。
所以,只要 left < right,说明仍有某些低位是不稳定的(在中间会变化),这些低位我们干脆整体右移"扔掉"。
当右移到 left == right 的那一刻,说明剩下的所有位在整个区间内已经完全一致,它们就是公共前缀。
最后再通过左移 shift 位,把这些高位放回原来的位置,低位补 0,得到最终按位与结果。
细节问题:while 条件与移位方向
关于你在过程中的几个思考点,顺便也整理一下:
left != right 还是 left < right?
题目保证 left <= right,输入不会出现 left > right。leetcode
又因为每一步都同时右移 left 和 right,不会人为制造 left > right 的情况。
所以 while (left < right) 和 while (left != right) 在这题上效果等价,用哪一个主要看你觉得哪一种更清晰。
算术右移还是逻辑右移?
左移只有一个操作符 <<,没有算术/逻辑之分。
右移在 C/C++/Java 中,>> 对有符号整数通常是"算术右移",保留符号位;Java 还有 >>> 是逻辑右移。
本题保证 0 <= left, right <= 2^31 - 1,全部是非负数,即最高位为 0,不会出现符号扩展问题,所以用 >> 即可,算术右移和逻辑右移在这里表现一致。leetcode
left == 0 需要特判吗?
不需要。left == 0 时,区间 [0, right] 一定包含 0,整个区间按位与结果必然为 0。
在上面的循环中:
left 右移始终为 0。
right 不断右移直到也变为 0,此时 left == right == 0。
最终返回 0 << shift,仍然是 0,符合预期,无需额外分支。
示例推演:验证思路
以几个典型例子推一遍这个过程,可以更有信心:
left = 6 (110),right = 7 (111):
第一次:left = 3 (11),right = 3 (11),shift = 1。
循环结束,返回 3 << 1 = 110 (6),与真实按位与结果一致。
left = 12 (1100),right = 15 (1111):
第一次:1100 -> 110,1111 -> 111,shift = 1。
第二次:110 -> 11,111 -> 11,shift = 2。
返回 3 << 2 = 1100 (12),与逐个按位与的结果一致。
left = 1,right = 2147483647:
不断右移后,最终两者都会变成 0,shift 为 31。
返回 0 << 31 = 0,符合题目示例。
总结:适合写成博客的结构建议
如果写成一篇博客,大致可以按下面结构组织:
题目描述与样例
直观思路:为什么要找公共前缀
错误/不完整思路:只看最高位 1 的陷阱
正确解法一:右移找公共前缀(你现在实现的解法)
思路描述
代码实现
示例推演
边界条件讨论(left == 0、移位类型等)
复杂度分析:时间 O(logN),空间 O(1)
小结:这类题的共性------"范围按位运算"往往等价于"端点的公共前缀"
https://leetcode.com/problems/bitwise-and-of-numbers-range/?envType=study-plan-v2&envId=top-interview-150
https://leetcode.com/problems/bitwise-and-of-numbers-range/submissions/1876552823/?envType=study-plan-v2&envId=top-interview-150