本节内容需要熟悉前两章内容。
题目:信息来自于201. 数字范围按位与 - 力扣(LeetCode)
给你两个整数 left
和 right
,表示区间 [left, right]
,返回此区间内所有数字 按位与 的结果(包含 left
、right
端点)。
示例 1:
输入:left = 5, right = 7 输出:4 5&6&7=4
示例 2:
输入:left = 0, right = 0 输出:0
示例 3:
输入:left = 1, right = 2147483647
输出:0 测试数据:0 <= left <= right <= 2147483647
题前点拨:
与运算‘&’的特点是有0则0,全1则1。
因此在数值上,a&b≤min(a,b)是一定成立的
并且如果a和b的二进制位数不同,这道题的结果一定是0
与运算的本质其实就是求两个数的二进制表达式的交集
所以这道题的本质,就是求left到right之间所有自然数的二进制表达式的交集
也就是求left和right这两个数之间的公共前缀部分
因为除了公共前缀部分之后,剩下的二进制位都一定会在遍历left到right的时候改变。
解①:
brian kernighan算法
class Solution {
public:
int rangeBitwiseAnd1(int left, int right) {
while (left < right) {
right -= right&-right;
}
return right;
}
int rangeBitwiseAnd2(int left, int right) {
while (left < right) {
right &= right-1;
}
return right;
}
};
通过brian kernighan算法来实现不断地把right的二进制表达式中的最右侧的1删去
并控制条件当left>=right时循环终止,可以实现求left和right的公共前缀所代表的数
举个例子,对于rangeBitwiseAnd1
如果 left = 5 (101) 和 right = 7 (111),函数的执行步骤如下:
- right (111) & -right (001) = 001
- right = right - 001 = 110
- 此时 left = 101,right = 110。
- 再次 right (110) & -right (010) = 010
- right = right - 010 = 100
- 此时 right (100) 不再大于 left (101),循环结束。
- 返回 right (100),也就是 4,这是区间 [5, 7] 所有数位与运算的结果。
同理: 对于rangeBitwiseAnd2
如果 left = 5 (101) 和 right = 7 (111),执行过程如下:
- right = right - 1 -> 111 - 1 = 110
- right &= 110 -> 110 & 110 = 110
- right = right - 1 -> 110 - 1 = 101
- right &= 101 -> 110 & 101 = 100
- 此时 right (100) 不再大于 left (101),循环结束。
- 返回 right (100),也就是 4,这是区间 [5, 7] 所有数位与运算的结果。
这个算法的时间复杂度是 ,准确来说是O(k)k=right中非公共部分中1的个数。
解②:
直接寻找公共前缀
class Solution {
public:
int rangeBitwiseAnd(int left, int right) {
int shift = 0;
while (left < right) {
left >>= 1;
right >>= 1;
shift++;
}
return left << shift;
}
};
这个函数实现的是通过直接寻找公共前缀的方法,该函数的工作流程如下:
-
初始化一个变量
shift
,记录left和right的非公共部分的位数。 -
当
left
比right
小的时候,进行如下操作:
a. 对left
和right
进行一次右移操作,即left >>= 1
和right >>= 1
,这里每执行一次右移,意味着我们抛弃了当前的最低位。
b. 计数变量shift
加一,这表示我们将最终结果中要补充一位0。 -
反复执行步骤2,直到
left
和right
相等。在这个点上,由于任何left
到right
之间的数都会影响最终的 AND 操作结果,所以当两者相等,我们确保了它们的公共前缀(即left
和right
变得相等那部分)是不变的。 -
当
while
循环结束后,我们得到了left
和right
的公共前缀部分,此时left
是这个公共前缀右移shift
位后的值。 -
最后,我们需要将
left
再左移shift
位,补回之前右移操作丢弃的位,这些位置都是0,因为在[left, right]
范围内的任何其他数字至少在其中一个位置为0。这样我们便得到了剩余的二进制位的 AND 结果,即公共前缀后跟上对应数量的0。 -
返回左移后的
left
值作为结果。
例如,如果 left = 26
(二进制为 11010
) 和 right = 30
(二进制为 11110
),函数的执行步骤如下:
- 初始时,
shift = 0
left = 11010, right = 11110
left >> 1 = 1101, right >> 1 = 1111, shift = 1
left >> 1 = 110, right >> 1 = 111, shift = 2
left >> 1 = 11, right >> 1 = 11, shift = 3
(这时left
和right
相等)- 出循环,最终
left = 11
(即二进制中的11
后跟shift=3
个0,即11000
) left << shift = 11000
(即二进制的11000
或十进制的 24)
最终返回24,是因为在 [26, 30]
范围内所有数字的位与运算结果。在 [26, 30]
范围内,只有 11000
是所有数字共有的二进制前缀。
这个算法的时间复杂度是,准确来说是O(k)k=right中非公共部分所有位数。
解③:
利用全1算法
class Solution {
private:
int getOne(int n) {
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n;
}
public:
int rangeBitwiseAnd(int left, int right) {
return ~getOne(left ^ right) & left;
}
};
首先left^right保留left和right的第一个非公共位信息
通过全1函数把left^right的所有位抹成1
然后对其取反,原本是1的位置全都变成了0,并且刚好对应left和right的所有非公共位
最后&left,得出公共前缀部分。
全1函数这里起到抹除信息的作用。
这个算法的时间复杂度是,且没有任何的条件语句,极大程度上优化了性能。