Every day a Leetcode
题目来源:201. 数字范围按位与
最直观的解决方案就是迭代范围内的每个数字,依次执行按位与运算,得到最终的结果,但此方法在 [left, right] 范围较大的测试用例中会因超出时间限制而无法通过,因此我们需要另寻他路。
我们观察按位与运算的性质。对于一系列的位,只要有一个零值的位,那么这一系列位的按位与运算结果都将为零。
对所有数字执行按位与运算的结果是所有对应二进制字符串的公共前缀再用零补上后面的剩余位。
这个结果实际上是这些数的二进制表示的最长公共前缀,后面补零。
解法1:位移
鉴于上述问题的陈述,我们的目的是求出两个给定数字的二进制字符串的公共前缀,这里给出的第一个方法是采用位移操作。
我们的想法是将两个数字不断向右移动,直到数字相等,即数字被缩减为它们的公共前缀。然后,通过将公共前缀向左移动,将零添加到公共前缀的右边以获得最终结果。
代码:
/** @lc app=leetcode.cn id=201 lang=cpp** [201] 数字范围按位与*/// @lc code=start
class Solution
{
public:int rangeBitwiseAnd(int left, int right){int shift = 0;// 找到公共前缀while (left < right){left >>= 1;right >>= 1;shift++;}return left << shift;}
};
// @lc code=end
结果:
复杂度分析:
时间复杂度:O(log(right)),算法的时间复杂度取决于 left 和 right 的二进制位数,由于 right 更大,因此时间复杂度取决于 right 的二进制位数。
空间复杂度:O(1)。我们只需要常数空间存放若干变量。
解法2:Brian Kernighan 算法
还有一个位移相关的算法叫做「Brian Kernighan 算法」,它用于清除二进制串中最右边的 1。
「Brian Kernighan 算法」的关键在于我们每次对 number 和 number − 1 之间进行按位与运算后,number 中最右边的 1 会被抹去变成 0。
基于上述技巧,我们可以用它来计算两个二进制字符串的公共前缀。
其思想是,对于给定的范围 [left, right],我们可以对数字 right 迭代地应用上述技巧,清除最右边的 1,直到它小于或等于 left,此时非公共前缀部分的 1 均被消去。因此最后我们返回 right 即可。
代码:
class Solution
{
public:int rangeBitwiseAnd(int left, int right){while (left < right){// 抹去最右边的 1right = right & (right - 1);}return right;}
};
结果:
复杂度分析:
时间复杂度:O(log(right)),和位移方法类似,算法的时间复杂度取决于 left 和 right 二进制展开的位数。尽管和位移方法具有相同的渐近复杂度,但「Brian Kernighan 算法」需要的迭代次数会更少,因为它跳过了两个数字之间的所有零位。
空间复杂度:O(1)。我们只需要常数空间存放若干变量。