位操作算法面试题

位操作算法面试题_第1张图片

方案一

我们可以利用为位与操作,依次判定各个位上是否为1。

    public int hammingWeight(int n) {//依次检测各个位
    	int count=0;
    	int location=1,value=1;
    	while(location<=32){
    		if((n&value)!=0){
    			count++;
    		}
    		location++;
    		value<<=1;
    	}
		return count; 
    }

陷阱

    public static int hammingWeight(int n) {//错误代码,不断检测最低位
    	int count=0;
    	int t=n;
    	while(t!=0){
    		count+=t&1;
    		t>>=1;
    	}
		return count; 
    }

如果我们通过不断检测最低位来统计1的个数,与此同时整数不断右移。对于正整数我们似乎可以得到正确的结果,那是因为右移后高位补0。

但是对于负整数,右移后高位补1,那么最终整数将会变成0xFFFFFFFF而进入死循环。

修正

    public static int hammingWeight(int n) {//不断检测最高位
    	int count=0;
    	int t=n,max=1<<31;
    	while(t!=0){
    		count+=(t&max)==0?0:1;
    		t=t<<1;
    	}
		return count; 
    }
对于左移,由于正数和负数低位都是补0。因此我们可以不断检测最高位来统计1的个数。

优化

    public static int hammingWeight(int n) {
    	int count=0;
    	int t=n;
    	while(t!=0){ 
    		t=t&(t-1);
    		count++;
    	}
		return count; 
    }
上面的算法复杂度为O(k),k为二进制的位数(这里为32)。而该方法将复杂度降低为O(m),m为二进制中1的个数,显然m<=k。

原理:例如n=1100 1100,n-1=1100 1011,n&(n-1)=11001000,不难发现,n&(n-1)可以消除最低位的那个1。

位操作算法面试题_第2张图片

分析

方案一:对于2的幂,其二进制表示中,1的个数必定只有一个。此外,还需要注意2的幂必为正数。

public class Solution {
    public boolean isPowerOfTwo(int n) {
        return hammingWeight(n)==1&&n>0;
    }
    public  int hammingWeight(int n) {
    	int count=0;
    	int t=n;
    	while(t!=0){ 
    		t=t&(t-1);
    		count++;
    	}
		return count; 
    }
}

方案二:我们知道n&(n-1)可以消除掉最低位的1,那么2的幂二进制位中只有一个1,因此我们只需判断n&(n-1)==0即可。

public class Solution {
    public boolean isPowerOfTwo(int n) {
        return (n&(n-1))==0&&n>0;
    }
}

方案三:我们知道正整数中最大的数是1<<30,我们直接判定1<<30能不能被n整除即可。

public class Solution {
    public boolean isPowerOfTwo(int n) {
        return n>0&&((1<<30)%n)==0;
    }
}

方案四:我们知道整数范围内2的幂就那么几个,我们直接用switch语句进行比较就可以了。略。

方案五:将n不断除以2,并且没有余数,即可判定其是否为2的幂。显然除法效率没有位移运算高效,我们可以利用右移代替除2。

位操作算法面试题_第3张图片
分析

方案一:首先我们可以判定其是否为2的幂,然后再判定1的位置。

    public boolean isPowerOfFour(int num) {
        if(!isPowerOfTwo(num)){
        	return false;
        }
        int location=1,value=1,n=num;
    	while(location<=32){
    		if((n&value)!=0){
    			break;
    		}
    		location++;
    		value<<=1;
    	}
        return location%2==0;
    }
    public boolean isPowerOfTwo(int n) {
        return (n&(n-1))==0&&n>0;
    }


如果不用循环或者递归,我们该如何处理呢?

方案二:对于1在哪个位置上的判断我们可以做如下优化。

public class Solution {
    public boolean isPowerOfFour(int num) {
        return num > 0 && (num&(num-1)) == 0 && (num & 0x55555555) != 0;
        //5的二进制表示为0101,因此0x55555555覆盖了所有我们期望1出现的位置
    }
}

方案三:对于区别4的幂和2的幂,还可以如下判断。

public class Solution {
    public boolean isPowerOfFour(int num) {
        return num > 0 && (num&(num-1)) == 0 && (num-1)%3==0;
        //n-1,的低位是连续的1,1的个数为偶数,而3的二进制位11,1111=1100+0011也必定是3的整数被,因此偶数个1就是3的整数倍。
    }
}

方案四:整数范围内4的幂就那么几个,我们直接利用switch语句进行比较就可以了。

位操作算法面试题_第4张图片

分析

方案一:整数中最大的3的幂为3^19(1162261467),如果3^19能被n整除,n就是3的幂。

public class Solution {
    public boolean isPowerOfThree(int n) {
        // 1162261467 is 3^19,  3^20 is bigger than int  
        return ( n>0 &&  1162261467%n==0);
    }
}

方案二:用switch语句。

方案三:可以不断除3,以判断余数。

位操作算法面试题_第5张图片
分析

对于a=0100 1100,b=0111 0100。

a^b= 0011 1000,代表各个位对应相加后该位的结果,不考虑进位。

a&b=0100 0100,表示各个位对应相加后的进位情况,我们需要将进位累加到高一位上去,(a&b)<<1=1000 1000。.

因此a+b=a^b+(a&b)<<1,1100 0000。加法的实质,也就是第一个操作数的特定位上需要进位,而第二个操作数指明是哪些位。

这样递归处理,最终每个位都不需要进位的时候,就得到了我们计算的结果。

	public int getSum(int a, int b) {
		if(b == 0){//没有进位的时候完成运算
	        return a;
	    }
	    int sum,carry;
	    sum = a^b;//第一步完成各个位上的加发运算
	    carry = (a&b)<<1;//第二步完成进位运算(左移运算)
	    return getSum(sum,carry);//
	}
位操作算法面试题_第6张图片 位操作算法面试题_第7张图片

分析

我们可以用等长的二进制位000分别表示对应元素是否选取。000表示三个元素都不选取,结果为[],101表示选取第一个和第三个元素,结果为[1,3]。

我们将二进制位从000一直加到111,就得到了所有的子集合。

public class Solution {
	int[] choose=null;
	private boolean isEnd(){//是否到结尾了,全选
		for(int i:choose){
			if(i==0) return false;
		}
		return true;
	}
	private void next(){//模拟二进制加1
		int add=1;
		for(int i=0;i> subsets(int[] nums) {
    	List> res=new ArrayList>();
    	if(nums.length==0){
    		res.add(new ArrayList());
    		return res;
    	} 
    	choose=new int[nums.length]; 
    	while(true){
    		List r=new ArrayList();
    		for(int i=0;i

对于组合问题,我们还可以利用回溯或者分期摊还的方法进行处理,参考:

https://discuss.leetcode.com/topic/19110/c-recursive-iterative-bit-manipulation-solutions-with-explanations

http://blog.csdn.net/sunxianghuang/article/details/51887505

位操作算法面试题_第8张图片
分析

如果a=b,那个a^b=0。因此,我们将所有元素进行异或就可以得到结果。

public class Solution {
    public int singleNumber(int[] nums) {
    	int res=0;
    	for(int i:nums){
    		res^=i;
    	}
    	return res;
    }
}
此外,我们还可以利用哈希表来进行频率统计,但是效率没有位运算高。

位操作算法面试题_第9张图片
分析

遍历元素,我们可以分别统计每个二进制位上出现1的次数。对于三个相同的元素,他们对特定位上1的个数的贡献肯定是3的倍数,因此我们将所有二进制位上1的次数%3,最后每个二进制位上的1就是那个单独元素贡献的,因此我们就可以计算出最终的结果了。

public class Solution {
    public int singleNumber(int[] nums) {
    	int length = nums.length;
    	int result = 0;
    	for(int i = 0; i<32; i++){  
            int count = 0;//统计所有元素,该位上1出现的次数   
            int mask = 1<< i;  
            for(int j=0; j

扩展

问题转换为:给定数组中,只有一个元素出现 P 次,其余元素都出现K 次,且p>=1&&p%k!=0,找出这个出现 P 次的唯一元素。

方法论

依照之前的思想,我们对每一个二进制位统计出现1的次数,然后对k求余。最终次数不为零的位,就是唯一元素二进制为1的位。

网上还存在一种全部使用位操作实现的算法,但很少给出具体解析,这里进行一下整理。

对于二进制位中1出现次数的统计,我们可以采用位操作实现,这样更加高效。分析如下:

1、如何用二进制来表示1出现的次数

对于一般元素,出现次数为 K ,要想表示整数K, 我们需要 m 个二进制位来表示( 2^m >= k)。由于我们需要统计整数的32个位,因此我们需要32*m个二进制位来统计1的出现次数。如下图所示,例如用X1{5}X2{5}....Xm{5}(低位->高位)组成的二进制数来表示第5个二进制位上1出现的次数。其中,Xi(1<=i<=m)是一个32位整数,Xi{5}表示整数Xi二进制表示中的第5个二进制位。

位操作算法面试题_第10张图片

2、如何用二进制位来统计1的次数

每处理一个元素 E ,特定位上要么是1要么是0。对于第5个二进制位,如果二进制数X1{5}X2{5}....Xm{5}的最高位Xm{5}想要增加(必须通过低位进位来实现,也就是说低位必须全部都为1且需要加上1),其需要满足的条件是:E 的第五个二进制位为1,记做 E{5}=1,且X1{5}==1&&X2{5}==1&&....&& Xm-1{5}==1。同理,Xi{5}想要增加的条件是,X1{5}==1&&X2{5}==1&&....&& Xi-1{5}==1。

因此,对于二进制数X1{5}X2{5}....Xm{5},Xi{5}增加的条件满足时我们需要对其+1,条件不满足时什么都不做。

回想一下,如果第二个操作数为0则第一个操作数不会改变的运算有哪些?你会想到x=x|0和x=x^0。 

这里我们利用异或运算(^),例如Xi{5}=Xi{5}^(X1{5}==1&&X2{5}==1&&....&& Xi-1{5}==1),如果条件不成立,Xi{5}保持不变,如果条件成立Xi{5}就是加1,原来是1就变成0,原来是0就变成1。这样,我们就实现了利用二进制进行1的次数统计。

3、统计次数的重置

例如,我们的为5,5需要3位二进制表示,5的二进制表示为"101",那么我们的统计次数达到"101"之后应该重新置为零,否则,继续往上统计达到“111”之后会产生溢出,统计就会出错。也就是说我们需要找到一个特定的条件,当这个条件满足时进行置零,条件不满足时什么都不会发生,这里我们依旧使用与运算(&)和非运算(~)。例如二进制数X1{5}X2{5}....Xm{5},代表第5个二进制位上1的次数,假设K为5,那么m=3,如果想要对其置零,需要满足的条件就是X1{5}==1&&X2{5}==0&&X3{5}==1。

X1{5}X2{5}....Xm{5}=X1{5}X2{5}....Xm{5} &(~ (X1{5}==1&&X2{5}==0&&X3{5}==1) )。这样我们实现了次数的重置。


分析到这里,我们现在跳出对单个二进制位的分析。而对整数中32个二进制位统一考虑。

因此对于次数统计,Xm=Xm^(Xm-1&..&X2&X1),这里利用与运算(&)代替条件判定。

对于次数的重置,Xm=Xm&condition,condition满足这样的性质:当且仅当1的次数统计为K时其结果为0,其余情况都为1。

condition= ~(x1' & x2' & ... xm'),where xj' = xj if kj = 1 and xj' = ~xj if kj = 0 (j = 1 to m),其中kj 就是K的二进制表示中第j个二进制位的值,例如K为5("101"),那么condition=~(X1&~X2&X3)。

对于此题,K=3(二进制表示"11")、P=1,因此m=2,condition=~(X1&X2)。得到如下算法:

public class Solution {
    public int singleNumber(int[] nums) {
        int x1 = 0;   
        int x2 = 0; 
        int condition = 0;
        for (int i : nums) {
            x2 ^= x1 & i;
            x1 ^= i;
            condition = ~(x1 & x2);
            x2 &= condition;
            x1 &= condition;
         }
        return x1; 
    }
}
这里的结果直接就是x1,因为p=1,二进制表示位“1”,x1中每个二进制位上的值就是最终结果对应二进制位上的值。如果p=2呢,2的二进制表示位“10”,因此x2中每个二进制位上的值就是最终结果对应二进制位上的值,而x1最终为零。

如果K=9、P=7,那么m=4,7的二进制位“0111”(高位到低位),因此x1、x2、x3中二进制位上的值都与结果对应二进制位上的值相同,因此都能作为最终结果,只有x4最终为零。

参考

https://discuss.leetcode.com/topic/11877/detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers

位操作算法面试题_第11张图片

分析

将所有元素异或后的结果即为5^3,5和3的二进制表示分别为0101和0011,异或的结果为0110,我们根据元素的二进制表示中从低位开始第二个(2^1)二进制位是否为1将数组划分为两部分[1,1,3],[2,3,2],这样问题就退化为问题137啦。

public class Solution { 
    public int[] singleNumber(int[] nums) {
        int[] res=new int[2];
        int two=singleNumberSimple(nums);
        int location=0; 
        while(location<=31){
        	if(((1< first=new ArrayList();
        ArrayList second=new ArrayList();
        for(int i:nums){
        	if(((1< nums) {
    	int res=0;
    	for(int i:nums){
    		res^=i;
    	}
    	return res;
    }
    public int singleNumberSimple(int[] nums) {
    	int res=0;
    	for(int i:nums){
    		res^=i;
    	}
    	return res;
    }
}

位操作算法面试题_第12张图片

分析

对于长度为n的数组,缺失数的范围为0~n。缺失,也就是说我们需要进行存在性判断,因此我们可以使用哈希表,有因为需要判定的数据在限定范围内,因此我们可以使用数组来实现哈希表,此外也可以使用HashMap记录0~n-1的存在性。

public class Solution {
    public int missingNumber(int[] nums) {
    	int n=nums.length;
    	if(n==0) return 0;
    	int[] mark=new int[n];
    	for(int i:nums){
    		if(i>=0&&i
此种解法的空间复杂度为O(n),时间复杂度为O(n)。那么如何将空间复杂度降低为O(1)呢?

我们可以利用数组本身来做存在性判定,如果存在值 i 我们保证a[i]存放的就是值 i ,然后依次检查a[i]上存放的是不是 i 即可。

public class Solution {
    public int missingNumber(int[] nums) {
    	int n=nums.length;
    	if(n==0) return 0;
    	for(int i=0;i
位操作算法面试题_第13张图片

分析

对于单词字符串的字符出现情况我们可以使用一个哈希表来记录,由于范围限定在‘a’~'z',我们可以直接使用26位的数组,由于只需标记字符是否出现(状态只有0和1),因此我们只需要用一个位来表示该字符是否存在,因此我们只需要一个32位的整型变量,其32个二进制位就可以标记字符出现情况,并且我们在匹配两个字符串出现字符是否有重复时,我们只需要使用位与(&)即可。

    public int maxProduct(String[] words) {
    	int res=0,n=words.length;
    	if(n==0) return res;
    	int[] mark=new int[n];
    	for(int i=0;i

位操作算法面试题_第14张图片

分析

n的二进制表示中1个数等于n>>1的二进制表示中1的个数+n的二进制表示中最低位1的个数(n&1)。因此,我们就避免了对每一个正数进行计算二进制中1的个数。

public class Solution {
    public int[] countBits(int num) {
    	int[] res=new int[num+1];
    	for(int i=1;i<=num;i++){
    		res[i]+=(res[i>>1]+(i&1));
    	}
    	return res; 
    }
}

变形

如果我们不是计算每个整数的二进制表示中1的个数。而是统计0至n所有整数的二进制表示中1的个数呢?或者更精确的讲,统计

每个二进制位上1的出现次数呢?

对于所有整数二进制表示中1的个数,显然可以按照上面的方法,先依次计算再进行求和。时间复杂度和空间复杂度都是O(n)。

而对于每个二进制位上1的出现次数,我们可以迭代所有整数,利用位与(&)判定各个位上的值,并进行统计,时间复杂度O(n)。

接下来介绍一种更加高效的算法,时间复杂度O(1)。

首先我们观察0000->1111的变化情况,低位到高位

0000

1000

0100

1100

0010

1010

0110

1110

0001

1001

0101

1101

0011

1011

0111

1111

发现第一个二进制位变化周期为1,第二个二进制位变化周期为2,第三个二进制位变化周期为4,第四个二进制位变化周期为8。

因为0~1的变化是呈现周期性的,故0000->1111,各个位上出现1的情况都是一半。总共2^4=16个数,每个位1出现次数为(2^4)/2。

现在我们来分析更一般的情况:0000 0000->0010 0101。我们将这样的变化过程进行分解为:

第一步:0000 0000->1111 1110

第二步:0000 0001->1111 1001

 第三步:0000 0101->1100 0101

第四步:0010 0101

第一步:0000 0000->1111 1110总共2^7个数,前7个二进制位,每个二进制位上1的个数(2^7)/2。

第二步:0000 0001->1111 1001总共2^5个数,前5个二进制位,每个二进制位上1的个数(2^5)/2,第8个二进制位永远是1。

第三步:0000 0101->1100 0101总共2^2个数,前2个二进制位,每个二进制位上1的个数(2^2)/2,第6、8个二进制位永远是1。

第四步:1个数,第3、6、8个二进制位是1。

因此我们不难得出如下算法:

    public int[] countBits(int num) {
    	int[] res=new int[32]; 
    	ArrayList indexs=new ArrayList();//1出现的位置
    	int location=30;//因为32位的int,正数高位为0
    	while(location>=0){
    		if((num&(1<>1;
    			}
    			indexs.add(location);
    		}
    		location--;
    	} 
    	for(int i:indexs){
    		res[i]+=1;
    	}
    	return res; 
    }

扩展

将问题扩展到十进制中。对于0~n的整数,分别统计0~9出现的次数?分别统计每个位上0~9出现的次数呢?










你可能感兴趣的:(数据结构与算法,位操作,位操作算法,面试题,位运算)