[位运算] 201. 数字范围按位与 (总结规律、寻找最长公共前缀:移位、n & n-1)

201. 数字范围按位与

题目链接:https://leetcode-cn.com/problems/bitwise-and-of-numbers-range/

关键点(分类)

  • 问题转化(按位与 → 寻找最长公共前缀)、
  • 位运算(寻找最长公共前缀:移位、n & n-1)

在这里插入图片描述

题目分析

位运算的很多题目都是寻找一个简单又有效的规律,将问题转化为一个更简单的问题,本题也属于这一种类型。

  • 思路1是用最直接的方法来解题,一下子就能想到,但效率低,因为没有分析讨论任何规律。
  • 思路2是寻找最长公共前缀,最终结果=最长公共前缀 + 后面全部置0。

所以这题的难点有3个:
1、暴力解的用例出错原因分析;
2、总结规律,将问题转化;
3、寻找最长公共前缀的方法。

思路1:暴力解 + 溢出处理

从m开始遍历到n,将每个数都进行与操作,得到最终的结果。

优化:当与计算后的结果=0时可以直接返回。

存在的问题
1、用例出错(for-i循环导致i溢出)
输入:
2147483646
2147483647
输出:
0
预期结果:
2147483646

分析:i溢出了。当i == 2147483647时,即i = Integer.MAX_VALUE,仍然会进入for循环,然后执行i++,我们期望的是i++之后 > 2147483647就退出for循环,但2147483647+1后得到的是Integer.MIN_VALUE,即-2147483648(这里的原因见补码那一篇博客),所以 i 又会重新进入for循环,直到i == 0,res &=i 所以res == 0 ,触发break条件,才退出循环。

所以,可以增加一个判断条件,当i == 2147483647时,退出循环。 问题解决。

2、效率较低

思路1的解法没有分析出规律,是单纯的暴力解法 + 溢出处理。

实现代码
class Solution {
    public int rangeBitwiseAnd(int m, int n) {
        if(m == 0 || (m == n)) return m;
        int res = m;
        for(int i = m + 1; i <= n; i++){
            res &= i;
            if(res == 0) break;
            if(i == 2147483647) break;
        }
        return res;
    }
}

思路2:寻找最长公共前缀(移位、n&n-1)

算法分析

先给出结论:将[m,n]所有数字按位与,就是保留[m,n]的最长公共前缀,然后把后面的位全部置0,就得到所有数字按位与的结果。

证明:假设[m,n]的最长前缀为前i个位,第i+1位开始出现不同,在第i+1位出现不同必然是从0->1,说明在[m,n]范围内存在两个数x和x+1,它们之间的关系为:前i位都相同,x的第i+1位是0,后续位全是1,x+1的第i+1位是1,后续位全是0,这样第i+1位以后的所有位数相与都等于0。

例如:[8,12]
    8   0000 1000
    9   0000 1001
    10  0000 1010
    11  0000 1011
    12  0000 1100

[8,12]的最长公共前缀是前5位00001,从第0位到第4位都是相同的,而第5位在[8,12]中出现了0和1,说明存在两个数x和x+1,且x的第5位是0,后续位全是1,x+1的第5位是1,后续位全是0,在这里x=11,x+1=12,这两个数相与则得到的结果从第5位开始到最后一位都为0.
而最长公共前缀部分相与后保持不变,所以[8,12]所有数字按位与得到:0000 1000.

  • 寻找这一规律还是挺难的,关键点在于发现最长公共前缀相与后是不变的;剩下位数的按位与结果可以完全被01111…和10000…控制。

所以,问题转化为寻找[m,n]的最长前缀问题

算法实现:如何寻找[m.n]的最长前缀?

寻找[m,n]的最长前缀,就是寻找m和n的最长前缀。下面介绍两种方法寻找m,n的最长公共前缀。

方法1:移位

m,n不断无符号右移>>>,直到两个数相等时,得到的就是最长公共前缀,然后再向左移动相同的位数,因为左移时低位会自动补0,所以可以实现将剩余位全部置0的效果,得到最终的按位与结果。

实现代码:

class Solution {
    public int rangeBitwiseAnd(int m, int n) {
        if(m == 0 || (m == n)) return m;
        int count = 0;//统计移动了几位
        while(m != n){
            m >>>= 1;
            n >>>= 1;
            count++;
        }
        //m再重新向左移动count位,低位自动补0,可以实现将剩余位全部置0的效果
        m <<= count;
        return m;

    }
}
方法2:n & n - 1

我们已经知道,n & (n - 1)可以将n的最右的1置0,可以利用这一操作寻找两个数的最长公共前缀。

例如:m = 10 , n = 12,即
m = 0000 1010
n = 0000 1100,将n 最右的1置为0,得到:
n = 0000 1000,因为此时n <= m ,所以从置0位的高一位开始到最高位就是m,n的最长公共前缀,
后面剩余的位全部置0,,我们可以发现此时的n就是最终的结果,所以直接返回此时的n即可。

实现代码

class Solution {
    public int rangeBitwiseAnd(int m, int n) {
        if(m == 0 || (m == n)) return m;

        while(n > m){
            n = n & (n - 1);
        }
        return n;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值