在刷LeetCode的过程中,「数字范围按位与」是一道非常经典的位运算题目,它看似简单,却藏着位运算的核心思维------抓共性、弃差异。今天就来详细拆解这道题,从问题本质到代码实现,帮你彻底搞懂背后的逻辑,轻松掌握解题技巧。
一、题目回顾:明确需求
题目很简洁:给定两个整数 left 和 right,代表区间 [left, right],返回这个区间内所有数字按位与的结果(包含左右两个端点)。
举个例子帮助理解:
-
当 left = 5,right = 7 时,区间内数字为 5(101)、6(110)、7(111),按位与结果为 100(4);
-
当 left = 0,right = 0 时,结果就是 0;
-
当 left = 1,right = 3 时,数字为 1(01)、2(10)、3(11),按位与结果为 0。
二、痛点分析:为什么不能暴力求解?
看到这道题,很多人的第一反应是「暴力遍历」:从 left 遍历到 right,依次对每个数字做按位与操作。但这种方法存在一个致命问题------效率极低。
如果 left 和 right 的范围很大(比如 left = 1,right = 10^9),遍历一次需要执行 10^9 次操作,会直接超时。所以,我们必须找到一种更高效的方法,跳出「遍历所有数字」的思维定式。
三、核心原理:按位与的特性 + 公共前缀
要解决这道题,首先要掌握「按位与」的核心特性:只要有一个数字的某一位是 0,那么所有数字在这一位上的按位与结果就是 0。
结合区间 [left, right] 的特点:区间内的数字是连续的,从 left 递增到 right,二进制表示中,高位的公共前缀是不变的,而低位会从 0 变到 1,再从 1 变到 0。
举个具体的例子,left = 5(101),right = 7(111):
-
5 的二进制:1 0 1
-
6 的二进制:1 1 0
-
7 的二进制:1 1 1
观察这三个数字的二进制,高位只有最左边的「1」是所有数字共有的(公共前缀),后面的两位(低位)在区间内出现了 0,所以按位与结果中,公共前缀保留,低位全部变为 0,最终结果就是 100(4)。
由此可得出解题核心:区间内所有数字的按位与结果,等于 left 和 right 二进制表示的「公共前缀」,后面补 0。
四、代码解析:如何找到公共前缀?
知道了核心原理,接下来就是如何用代码找到 left 和 right 的公共前缀。这里用到了一个巧妙的操作------右移,具体逻辑如下:
typescript
function rangeBitwiseAnd(left: number, right: number): number {
let shift = 0;
// 找到公共前缀:不断右移,直到left和right相等
while (left < right) {
left >>= 1; // left右移1位,相当于去掉最右边的一位
right >>= 1; // right右移1位,去掉最右边的一位
++shift; // 记录右移的次数(也就是需要补0的位数)
}
// 把公共前缀左移shift位,补全后面的0
return left << shift;
};
代码逐行拆解:
-
定义 shift 变量,用于记录右移的次数(也就是公共前缀后面需要补 0 的位数);
-
循环条件:当 left < right 时,说明两者的二进制还没有完全重合(公共前缀还没找到);
-
left >>= 1 和 right >>= 1:将 left 和 right 同时右移 1 位,相当于去掉各自最右边的一位,逐步向公共前缀靠近;
-
循环结束后,left 和 right 已经相等,此时的值就是两者的公共前缀;
-
left << shift:将公共前缀左移 shift 位,补全后面的 0,得到最终的按位与结果。
五、实例验证:代入代码看效果
还是以 left = 5(101),right = 7(111)为例:
-
初始:left = 5(101),right = 7(111),shift = 0;left < right,进入循环;
-
第一次循环:left = 5 >> 1 = 2(10),right = 7 >> 1 = 3(11),shift = 1;left < right,继续循环;
-
第二次循环:left = 2 >> 1 = 1(1),right = 3 >> 1 = 1(1),shift = 2;left = right,循环结束;
-
返回 left << shift = 1 << 2 = 4(100),与预期结果一致。
六、边界情况补充
这道题的边界情况需要特别注意,避免踩坑:
-
当 left = right 时:区间内只有一个数字,结果就是 left(或 right),此时循环不执行,shift = 0,返回 left << 0 = left,符合预期;
-
当 left = 0 时:无论 right 是多少,区间内包含 0,按位与结果一定是 0(因为 0 与任何数按位与都是 0),代码也能正确处理(比如 left=0,right=5,循环后 left 和 right 都会变成 0,shift 为 3,返回 0 << 3 = 0);
-
当区间跨过大数(如 left=109,right=109+5):代码只需执行几次右移就会找到公共前缀,效率极高,不会超时。
七、总结:解题思维提炼
这道题的关键,是跳出「暴力遍历」的思维,抓住「按位与的特性」和「区间数字的二进制共性」------连续数字的二进制,只有高位公共前缀是不变的,低位一定会出现 0,导致按位与结果为 0。
通过「右移找公共前缀,左移补 0」的操作,我们将时间复杂度从 O(n) 优化到了 O(log n)(n 为 right - left 的差值),这也是位运算题目最核心的解题思路:用位操作替代遍历,利用二进制特性简化计算。