位运算总结(默认学过位操作符的知识):
1.
这六种就是常见的位运算符,无进位相加就是在二进制中,两个数的某一位1和1可以进位,但是异或就不进位,相加后为0,跟相同为0,相异为1一个意思
2.
第一小类题型,第几位就用右移或左移操作符来控制,是0还是1就&1就能判断了
3.
第二小类题型,要修改成1,那么就 | 1就行了,修改哪一位就让1左移多少位即可
4.
第三小类题型,修改为0,那就是& 0即可,但是只是让第x位,所以其他位要为1,那么就只能让1左移x位,然后取反再按位与即可
其中第2—4题属于一个大类题型,即通过左移右移再加上一些位运算即可解决
5.
什么是位图,类比于哈希表,哈希表是用一个数组,然后用下标与对应的值形成映射关系,以O(1)的时间复杂度来查询
而位图则不是用数组,而是一个变量的二进制位,从最右边为0下标开始,每个下标里面存的值都为0或1,0代表一种情况,1代表另一种情况,这就是位图的思想。(注意虽然有32位比特位,但因为从0下标开始,所以最左边为31而不是32 )
6.
这里正数变为负数的变化规则是取反加1,所以就使得最右侧的1的左边区域全部取反,而右边区域全部为0,自身1不变
所以这时候就让正数和负数&,左边区域因为都相反,互为01,则全部为0,右边区域都为0所以也全部为0,就只剩最右侧的1了
7.
n-1的操作就是让最右侧的1的右边区域全部取反,然后自身变为0,左边区域不变,其实很好理解,因为最右侧的1的右边区域肯定全部为0或者它就是第0位,那么减1就要往前借位,最先借到的那肯定就是最右侧的1
这时再&原来的n,因为最右侧的1的右边区域和本身都相反,那么&之后都为0,就干掉了
8.位运算的优先级
不会真的期待着拿一个优先级表出来一个一个看和背吧,加括号就完事了,光位运算符就有6种,那再加上&=,|=,^=,&&,||,==,++,--那么多运算符结合在一起还能背的下来嘛,万一记错了又怎么办,所以不要背,加括号就完事了
9.
主要是要知道^符合交换律,即有a,b,c,d等等多个数,先a^b还是先a^d,顺序都无所谓的,最后全部^之后结果是唯一,证明也很好证明,就是无进位相加,在同一位上每出现2个1就直接划掉即可
综上,可以练习五道基础的位运算题
题目一:
思路:
全部异或就行了,但是变量值一开始要初始化为0,因为a^0=a,不能是1,不然就变成取反了
代码:
class Solution {public int singleNumber(int[] nums) {int ret=0;//初始化为0,因为a^0=afor(int x:nums){ret^=x;//全部异或,出现两次的全部抵消,因为a^a=0}return ret;}
}
题目二:
思路:
跟上一题的区别就是从只出现一次的1个元素变为了2个元素,直接全部异或下来只能得到这两个元素的异或值,这时无法根据异或值反过来判断是哪两个元素,因此只能想想其他的思路
这个异或值中,最右侧的1这一位就是这两个元素二进制位中第一次出现不一样的地方,所以可以从这一位来进行分组,所以用x^(-x)来提取这个数
再遍历一次,设置两个变量x1,x2,如果与这个数异或后为0,说明这个数的这一位为0,分为第一组并与x1进行异或,反之,如果为1则说明这个数的这一位为1,分为第二组并与x2进行异或
最后就保证了那两个元素一定分开了不在同一组,此时x1,x2全部异或完后剩下的那个值就分别为这两个元素
代码(修正前):
class Solution {public int[] singleNumber(int[] nums) {//找到只出现一次的两个元素的异或值int ret=0;for(int x:nums){ret^=x;}//提取异或值的最右侧的1ret=ret&(-ret);//分为两组int x1=0,x2=0;for(int x:nums){if((x&ret)==0){//如果在这一位上为0x1^=x;//将该组再全部异或}else{//这一位上为1x2^=x;//将该组再全部异或}}return new int[]{x1,x2};}
}
但是有一点小漏洞,也是看官方题解才注意的,那就是整型的最小值的负数溢出了整型的最大值
,有溢出风险,因为最小值为-2147483648(即-2^31),而最大值为2147483647(即2^31-1),
所以要处理一下,而-2147483648的二进制数为1000……000,所以最右侧的1就为第一位,所以它本身就是提取最右侧的1的数,所以不变即可,因此上面的代码在提取的时候要修改一下
代码2(修改后):
class Solution {public int[] singleNumber(int[] nums) {//找到只出现一次的两个元素的异或值int ret=0;for(int x:nums){ret^=x;}//提取异或值的最右侧的1ret=(ret==Integer.MIN_VALUE?ret:ret&(-ret));//分为两组int x1=0,x2=0;for(int x:nums){if((x&ret)==0){//如果在这一位上为0x1^=x;//将该组再全部异或}else{//这一位上为1x2^=x;//将该组再全部异或}}return new int[]{x1,x2};}
}
题目三:
思路:
就是返回一个数的二进制中有多少个1,那么就直接用n&(n-1)这个公式来解决,这个公式每次都消除最右边的一个1,那么只需要循环,每次消除一个,计数器就加1,直到没有1了就退出循环
代码:
class Solution {public int hammingWeight(int n) {//计数器int count=0;//当n的二进制中还有1时while(n!=0){//消除一位1n=n&(n-1);//计数器加1count++;}//返回个数return count;}
}
题目四:
思路:
跟上一道题一样,也是要计算1的个数,只是变成了要将个数保存到数组中(还可以更高效,但需要用到动态规划,就先用这个方法即可)
代码:
class Solution {public int[] countBits(int n) {//设置长度为n+1的数组int[] ret=new int[n+1];//遍历for(int i=0;i<=n;i++){//计算该数有多少个1int x=i;int count=0;while(x!=0){x=x&(x-1);count++;}//将个数保存到数组ret[i]=count;}return ret;}
}
题目五:
思路:
通过异或来找到不同的位,其中不同的位为1,再用n&(n-1)即可找到有多少个1
代码:
class Solution {public int hammingDistance(int x, int y) {//通过异或找到不同的位int ret=x^y;int count=0;//其中异或值为1的就代表不同while(ret!=0){ret=ret&(ret-1);count++;}return count;}
}
基础题做完,然后接下来就会是增加一点难度的几道题
题目六:
思路:
无论什么题,先暴力,后面再优化,这道题暴力很简单,就是用两层循环,第一层遍历整个字符串,第二层遍历该位置之后的字符,如果出现相同的就返回false,循环结束了之后,还没有返回false就说明里面没有重复的,返回true即可,时间复杂度为O(N^2)
然后就是优化,可以用哈希表来构建映射关系,出现的字符和出现的次数构成映射关系,如果该字符getOrDefault(ch,0)时出现的次数为0,就put(1),如果不为0,就说明之前出现过,返回false,遍历完字符串如果还没有返回false,说明里面没有重复的,返回true即可,时间和空间复杂度都是O(N)
但是这道题说了只包含小写字母,所以没必要真正创建一个哈希表,用int[]模拟哈希表即可,长度为26
但是哈希表还不是最优的,最后就是位图的思想,正如题目所说,如果不使用额外的数据结构,会很加分,我们不用哈希表,数组,只用一个int变量,以该int的32比特位替代哈希表即可
具体操作就是比特位从右开始为第0位,代表a,依次往左代表b,c……,如果该字符对应的那一位&1的结果为0,说明该字符没有出现过,就将那一位 | 1,如果&1的结果不为0,说明该字符已经出现过了,返回false,遍历完字符串如果还没有返回false,说明里面没有重复的,返回true即可
还有个小优化,有时候题目给出的通过的案例的字符串会很长,但是我们都知道小写字符就只有26个,如果长度大于26了,那么肯定有一个小写字符重复了,这也是数学的鸽巢原理,所以在一开始判断一下字符串长度,如果大于26直接就返回false
代码:
class Solution {public boolean isUnique(String astr) {//利用鸽巢原理判断一下if(astr.length()>26){return false;}//转为字符数组char[] ch=astr.toCharArray();//利用位图的思想int bitMap=0;for(int i=0;i<ch.length;i++){if(((bitMap>>(ch[i]-'a'))&1)!=0){//如果那一位不为0,说明重复return false;}else{//为0说明没有重复bitMap|=(1<<(ch[i]-'a'));//更新为1,代表出现过}}return true;//说明整个字符串都没有出现重复}
}
题目七:
思路1(排序加查找):
先将数组进行从小到大排序,然后遍历,如果下标等于对应的值,就往后遍历,直到不等于的那一个,该下标就为缺失的那一个
在查找这一步还可以优化一下,不用一个一个遍历,可以用我们之前的二分查找,如果数组的中间值mid小于0~n的中间值,说明mid~n这一区间少了,反之如果大于,则说明0~mid这一区间少了,其中采取如果偶数则mid值取左边那一个,直到旧mid==新mid,说明只剩两个数了,那么mid+1就是缺失的那一个
代码1(排序加二分):
class Solution {public int missingNumber(int[] nums) {//排序Arrays.sort(nums);//解决[1,2,3]第一位缺失0这种特例if(nums[0]!=0){return 0;}//解决[0,1,2]缺失最后一位这类情况if(nums[nums.length-1]!=nums.length){return nums.length;}//二分int left=0,right=nums.length-1;int mid=0;while(left<right){mid=left+((right-left)/2);if(mid<nums[mid]&&left+1!=right){//说明在0~mid区间right=mid;}else if(mid>=nums[mid]&&left+1!=right){//说明在mid~nums.length区间left=mid;}else{//说明mid对应的值前面这个就是缺失的return nums[mid]+1;}}//照顾编译器return -1;}
}
因为有答案不在二分区间的两种情况,所以要特判一下,然后就是用二分去找,比较麻烦,单纯只是可以练习一下二分
思路2(哈希):
用哈希数组来存出现的情况即可
代码2(哈希数组):
class Solution {public int missingNumber(int[] nums) {//创建哈希数组int[] hash=new int[nums.length+1];//记录出现情况for(int x:nums){hash[x]=1;}//再遍历0~nfor(int i=0;i<=nums.length;i++){if(hash[i]==0){//如果没有出现,说明该数缺失return i;}}//照顾编译器return -1;}
}
思路3(高斯求和):
利用0~n是等差数列来求和,再减去数组的和,就能找到缺失的数
代码3(高斯求和):
class Solution {public int missingNumber(int[] nums) {int sum=0;//求数组的和for(int x:nums){sum+=x;}//等差数列求和公式int gs=((0+nums.length)*(nums.length+1))/2;//返回缺失的值return gs-sum;}
}
思路4(位运算):
利用异或消消乐的性质,将数组的值和0~n全部异或,最后异或的值就是缺失的值
代码4(异或):
class Solution {public int missingNumber(int[] nums) {//异或的值初始化为0int ret=0;//遍历异或for(int i=0;i<nums.length;i++){ret^=nums[i];ret^=i;}//循环少异或了一个,补上ret^=nums.length;//返回缺失的return ret;}
}
题目八:
思路:
不使用运算符的话,大概率都是对二进制形式进行操作
之前我们说过异或^的特性除了相同为0,相异为1,还有无进位相加,即可以将两个操作数进行二进制的相加,但是进位却消失了
所以现在的问题就是如何找到缺失的进位,其实发生进位的情况就只有11这种情况,00,10,01都不发生进位,所以规律就是有0就为0,那就是&的性质,但是&之后只是对应的位为1,光找到了要进位的位置,却没有往前进位,依次往前进位的操作就需要<<
但是进位后有可能进行发生新的进位,所以要用循环,直到要进位的数为0
代码:
class Solution {public int getSum(int a, int b) {int ret=0;//表示无进位相加的结果int carry=0;//表示进位的数while(b!=0){//当要进位的数不为0ret=a^b;//异或求得无进位相加的值carry=(a&b)<<1;//算出进位的数//为下一次无进位相加和求进位的数赋值a=ret;b=carry;}return a;}
}
负数其实也没有问题的,比如-2+3,2的二进制为111111……1110因为进位会把前面的1全部顶替掉,然后最高位再左移就抛弃掉,然后最右边补0,也是没有问题的
蛮难想到的,算是一道典例
题目九:
思路1:
比较容易想到的方法是先排序然后再筛选,如果这个数往后出现了三次就说明不是,如果后面没有出现三次就说明是这个数
代码1(排序+筛查):
class Solution {public int singleNumber(int[] nums) {//先排序Arrays.sort(nums);//遍历数组for(int i=0;i<nums.length;i+=3){//如果不是最后一个数并且当前与后面两个都一样if(i+1!=nums.length&&nums[i]==nums[i+1]&&nums[i]==nums[i+2]){continue;}else{//不一样return nums[i];}}//照顾编译器return -1;}
}
思路2:
直接用哈希表,第一次遍历put,第二次遍历get,如果不为3就返回该值
代码2(哈希表):
class Solution {public int singleNumber(int[] nums) {//创建哈希表HashMap<Integer,Integer> map=new HashMap<>();//放入for(int x:nums){map.put(x,map.getOrDefault(x,0)+1);}//查找for(int x:nums){if(map.get(x)!=3){return x;}}//照顾编译器return -1;}
}
思路3:
这道题还可以用位运算来解决,因为都是整型,所以都有32位比特位,因为其他元素都出现三次,所以在第i位比特位上,我们设所有出现三次的元素的和为 n=(A+B+C……+X),那么出现了三次,总共的和就为3n,而此时加上只出现一次的元素的第i位比特位,就为3n+i,而i不是0就是1,此时再将和3n+i对3进行求余操作,(3n+i)%3=n……i,这样就可以求得只出现一次的元素第i位比特位为0还是1了,这样循环32次就可以将该元素求出来了
代码(位运算):
class Solution {public int singleNumber(int[] nums) {//结果int ret=0;//遍历32位比特位for(int i=0;i<32;i++){//所有数的第i位之和int sum=0;//求和for(int x:nums){sum+=((x>>i)&1);}//看只出现一次的元素第i位是0还是1sum=sum%3;//修改ret=ret|(sum<<i);}return ret;}
}
其实这道题要求时间复杂度为O(n),空间复杂度为(1),而上面的方法都不满足,真正符合要求的是用数字电路设计来解决,但是没学过并且这篇主要是练习位运算的,所以就不讲了
题目十:
思路1:
因为是连续的,所以可以先将数组从小到大排序,然后进行遍历,其中i用来表示下标,j用来表示1到N,如果i下标的值等于j,说明j这个数每缺失,此时i++,j++,如果不等于则说明j这个数缺失了,此时j++,但i不++,因为i对应的值还没有用来判断与对应的j那个数,同时将j的值保存在要返回的数组中
但是这样会有两种特殊情况,第一种就是[1],因为循环一进去根本不遍历到2,3,所以此时特判一下,如果返回的数组第一个元素为0,则返回的数组第一个元素为length+1,第二个元素为length+2
第二种就是[2,3],这样会漏1个元素“4”,此时返回的数组第一个元素为1,但第二个元素为0,所以此时特判一下,如果返回的数组第二个元素为0,则返回的数组第二个元素为length+2
代码1:
class Solution {public int[] missingTwo(int[] nums) {//先排序Arrays.sort(nums);//用来记录返回数组此时的下标int dis=0;int[] ret=new int[2];for(int i=0,j=1;i<nums.length;j++){//如果有匹配,那就没有缺失if(nums[i]==j){i++;}else{//没匹配到,缺失了ret[dis++]=j;//记录}}//如果漏了2个if(dis==0){ret[0]=nums.length+1;ret[1]=nums.length+2;}//如果只漏了1个if(dis==1){ret[1]=nums.length+2;}//返回数组return ret;}
}
时间复杂度为O(nlogn+n),空间复杂度为O(1),在时间复杂度上不符合要求,但是方法想起来比较简单
思路2:
其实这道题可以用题目二和题目七的方法相糅合即可,就是一开始先将数组的值和1~N全部异或,找出两个缺失的值的异或值,即题目七的异或消消乐方法,然后再将这个异或值进行分组异或,即题目二的分组异或,就完成了
class Solution {public int[] missingTwo(int[] nums) {//找到两个缺失的数的异或值int x1=0,x2=0;int ret=0;for(int x:nums){ret^=x;}for(int i=1;i<=nums.length+2;i++){ret^=i;}//找到最右侧的1并提取ret=ret&(-ret);//分组异或for(int x:nums){if((x&ret)==0){x1^=x;}else{x2^=x;}}for(int i=1;i<=nums.length+2;i++){if((i&ret)==0){x1^=i;}else{x2^=i;}}//返回数组return new int[]{x1,x2};}
}
时间复杂度为O(N),空间复杂度为O(1)
至此位运算的算法知识和练习就完毕了,总结就是那几个常用的公式得记一记并理解,比如提取最右侧的1是:n&(-n),消掉一个1是:n&(n-1)等等,然后就是多了一个有些情况能替代哈希表的位图这个方法,剩下就是一些综合运用了,多多练习,记得复习!