[位运算] 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;
}
}