位运算chapter2:求[left,right]所有自然数的与运算

本节内容需要熟悉前两章内容。

题目:信息来自于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(\log n) ,准确来说是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;
    }
};

这个函数实现的是通过直接寻找公共前缀的方法,该函数的工作流程如下:

  1. 初始化一个变量 shift,记录left和right的非公共部分的位数。

  2. 当 left 比 right 小的时候,进行如下操作:
    a. 对 left 和 right 进行一次右移操作,即 left >>= 1 和 right >>= 1,这里每执行一次右移,意味着我们抛弃了当前的最低位。
    b. 计数变量 shift 加一,这表示我们将最终结果中要补充一位0。

  3. 反复执行步骤2,直到 left 和 right 相等。在这个点上,由于任何 left 到 right 之间的数都会影响最终的 AND 操作结果,所以当两者相等,我们确保了它们的公共前缀(即 left 和 right 变得相等那部分)是不变的。

  4. 当 while 循环结束后,我们得到了 left 和 right 的公共前缀部分,此时 left 是这个公共前缀右移 shift 位后的值。

  5. 最后,我们需要将 left 再左移 shift 位,补回之前右移操作丢弃的位,这些位置都是0,因为在 [left, right] 范围内的任何其他数字至少在其中一个位置为0。这样我们便得到了剩余的二进制位的 AND 结果,即公共前缀后跟上对应数量的0。

  6. 返回左移后的 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(\log n),准确来说是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函数这里起到抹除信息的作用。

这个算法的时间复杂度是O(\log \log n),且没有任何的条件语句,极大程度上优化了性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值