(leetcode 201)数字范围按位与的超级详解(暴力法以及剪枝、数学分析法之自顶向下和自底向上)

原题如下:
在这里插入图片描述参考链接:
1、分析法参考题解链接
2、剪枝法参考题解:
“Transform”在上面分析法链接下的评论,未找到链接还请谅解

开始之前,我们需要想明白一个问题,在每位数按位与的时候,只要出现一次0,这一位的结果必定为0,这是本文优化的必经之路。

1、暴力法以及剪枝

这道题乍一看挺简单的,很容易写出O(n-m)复杂度的暴力法,即将[m,n]范围的所有数字都与一遍,如下:

class Solution:
    def rangeBitwiseAnd(self, m: int, n: int) -> int:
        ret=m
        for index in range(m+1,n+1):
            ret&=index
        return ret

这里有可以改进的地方,
1、当ret==0的时候,以后再与任何数字都是0,可以直接返回0;
2、当较大数n的二进制长于较小数m的二进制时,可以直接返回0,解释如下:

例如1xxxxx和1yy,
(1)从1yy到1xxxx的过程中必定包括10000,这样按位与以后最终结果除最高位外必定为0。
(2)1yy的对应到第五位的高位上的数字为0,因此最高位与的结果也是0。
(3)所以直接返回0即可。

同理,其他所有长度不同的情况按位与以后也是这样。
改进后的代码如下:

class Solution:
    def rangeBitwiseAnd(self, m: int, n: int) -> int:
        if len(bin(n))>len(bin(m)):
            return 0
        ret=m
        for index in range(m+1,n+1):
            ret&=index
            if ret==0:
                return 0
        return ret

2、数学分析法

最重要的是找一个位置,在m和n的二进制数中,从前到后第一个n这个位置为1而m为0的位置。
这个位置之前所有数字相等,这个位置之后按位与的结果必定为0。

为什么按位与的结果为0?

以数字1xx0yy,1xx1zz为例,
1、对于相等的前三位,我们不做处理,因为他们之间的所有数字这三位都相等,按位与之后的结果不会变化。
2、对于后三位数字0yy和1zz,他们之间的所有数字按位与的过程中必定经过100,因此除最高位之外的两个位置按位与之后最终的结果为0,而0yy的最高位为0,所以最后三位按位与之后的结果都为0。
最终返回的数字是1xx000。

下面我们介绍两种找这个位置的办法:

(1)自顶向下:

在m和n的二进制数中,从前到后找第一个不相等的数字比较简单,上代码:

class Solution:
    def rangeBitwiseAnd(self, m: int, n: int) -> int:
        if len(bin(n))>len(bin(m)):
            return 0
        m1,n1=bin(m),bin(n)
        #ok记录着从前到后相等的位数
        for i in range(len(m1)):
        	#如果前面这些位置相等
            if m1[i]!=n1[i]:
                return int(m1[:i]+"0"*(len(m1)-i),2)
        return m

(2)自底向上

与优化法相同的思路,我们还可以从后往前(自底向上)来找这个位置。
如果说n>m,表示此时的n某个位置是1,而m的位置是0,那么我们利用这个公式:

n&=n-1

使得n最后一位1为0变为0
如果n依然大于m,则再利用这个公式,把此时的最后一位1变成0,直到n<=m,返回n即可。

这时候为什么返回n是正确的,我们分情况讨论
(1)如果n的二进制位数大于m,则n一直到最后,也就是这样:100000…000的时候依然会大于m,继续将最后一个1变为0,则n为0,返回0即可,与自顶向下时对这种情况的讨论相同;
(2)如果n与m二进制位数相等,对于所有n>m的情况,从前到后n一定有一位为1,而m这一位为0,在改掉最前面的符合这个条件的位置之前,n会一直大于m。改掉之后n与m前面的数字都相等,这个位置t就是我们找的位置,此时n前面位置没变,后面位置都已经置为0,返回n即可。
上代码(这是学习大佬的最快也最简洁的方法,只是不太容易理解,笔者可以举几个例子在纸上演算,或者用断点来调,或者查看参考题解学习一下):

class Solution:
    def rangeBitwiseAnd(self, m: int, n: int) -> int:
        while n>m:
            n&=n-1
        return n

3、上述方法之间的比较:

看到这里读者应该发现了,两种方法殊途同归,只是按照笔者思考的过程分类。
(1)其本质都是只要两数的同一位置出现一个1一个0就停止后续的判断,
(2)区别在于暴力法的优化只针对最高位,剪去了位数不同时,最高位n是1,而m是0的情况;
分析法扩大到了从前到后面的每个位置,第一次一个为1一个为0的情况,从而剪掉了后面的判断,提前停止循环。

所以数学分析法应该是更快一些的。

‘’‘
一点可能的优化,为了不影响读者对方法的理解放在最后,对于真的超级长的数字,
自顶向下的数学分析法可以继续优化:
当前找第一次一个为1一个为0的位置是在for循环中,从前到后遍历来找,其实我们可以通过二分来找:
(1)如果某个位置前的数字相等,表示要找的位置在后面,则left=mid+1;
(2)否则表示我们找的位置在前面,则right=mid-1;
直到找到符合要求的位置。
‘’‘

欢迎讨论以及指出不足~

猜你喜欢

转载自blog.csdn.net/qq_41584385/article/details/106202564