位运算隐藏在编程语言的角落中,其神秘而又强大,暗藏内力,有些人光听位运算的大名的心中忐忑,还有些人更是一看到位运算就远远离去,我以前也是。但狡猾的面试官每每喜欢搞偷袭,抓住咱们的弱点搞咱们,为了防患于未然,特记此篇!java
本篇的内容为位运算的介绍和一些比较经典的位运算问题进行介绍分析,固然,位运算这么牛,后面确定仍是要概括总结的。git
什么是位运算?github
程序中的全部数在计算机内存中都是以二进制的形式储存的。位运算就是直接对整数在内存中的二进制位进行操做。
位运算就是直接操做二进制数,那么有哪些种类的位运算呢?面试
常见的运算符有与(&)、或(|)、异或(^)、取反(~)、左移(<<)、右移(>>是带符号右移 >>>无符号右移动)。下面来细看看每一种位运算的规则。算法
位运算 & (与)编程
规则:二进制对应位两两进行逻辑AND运算(只有对应位的值都是 1 时结果才为 1, 不然即为 0)即 0&0=0
, 0&1=0
, 1&1=1
数组
例如:2 & -2数据结构
位运算 | (或)编程语言
规则:二进制对应位两两进行逻辑或运算(对应位中有一 个为1则为1) 即0|0=0
,0|1=1
,1|1=1
函数
例如:2 | -2
位运算 ^ (异或)
规则:二进制对应位两两进行逻辑XOR (异或) 的运算(当对应位的值不一样时为 1, 不然为 0)即0^0=0
, 0^1=1
, 1^1=0
例如:2 ^ -2
按位取反~
规则:二进制的0变成1,1变成0。
移位运算符
左移运算<<
:左移后右边位补 0
右移运算>>
:右移后左边位补原最左位值(多是0,多是1)
右移运算>>>
:右移后左边位补 0
<<
没有悬念右侧填个零不管正负数至关于整个数乘以2。>>>
和左侧补原始位>>
,若是是正数没争议左侧都是补0,达到除以2的效果;若是是负数的话左侧补0>>>
那么数值的正负会发生改变,会从一个负数变成一个相对较大的正数。而若是是左侧补原始位(负数补1)>>
那么整个数仍是负数,也就是至关于除以2的效果。下面这张图能够很好的帮助你理解负数的移位运算符:
到这里,我想你应该对位运算有了初步的认识,在这里把上面提到的部分案例执行对比一下让你看一下可能会理解的更清晰:
在这里有些经常使用的位运算小技巧。
正常判断奇数偶数的时候咱们会这样写:
if( n % 2 == 1) // n 是个奇数 }
使用位运算能够这么写:
if(n & 1 == 1){ // n 是个奇数。 }
其核心就是判断二进制的最后一位是否为1,若是为1那么结果加上2^0=1必定是个奇数,不然就是个偶数。
对于传统的交换两个数,咱们须要使用一个变量来辅助完成操做,可能会是这样:
int team = a; a = b; b = team;
可是使用位运算能够不须要借助额外空间完成数值交换:
a=a^b;//a=a^b b=a^b;//b=(a^b)^b=a^0=a a=a^b;//a=(a^b)^(a^b^b)=0^b=0
原理已经写在注释里面了,是否是感受很是diao呢?
在遇到子集问题的处理时候,咱们有时候会借助二进制枚举来遍历各类状态(效率大于dfs回溯)。这种就属于排列组合的问题了,对于每一个物品(位置)来讲,就是使用和不使用的两个状态,而在二进制中恰好能够用1和0来表示。而在实现上,经过枚举数字范围分析每一个二进制数字各符号位上的特征进行计算求解操做便可。
二进制枚举的代码实现为:
for(int i = 0; i < (1<<n); i++) //从0~2^n-1个状态 { for(int j = 0; j < n; j++) //遍历二进制的每一位 共n位 { if(i & (1 << j))//判断二进制数字i的第j位是否存在 { //操做或者输出 } } }
有了上面的位运算基础,咱们怎么用位运算处理实际问题呢?或者有哪些经典的问题能够用位运算来解决呢。
题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
分析:
这道题咋一听可能没啥思路,简单研究一下位运算仍是能独立推出来和理解的。
固然,解决这题前,须要了解上面的四种位运算。还要知道二进制的运算:0+0=0,0+1=1,1+1=0(进位)
对于加法的一个二进制运算。若是不进位那么就是很是容易的。这时候相同位都为0则为0,0和1则为1.知足这种运算的异或(不相同取1,相同取0)和或(有一个1则为1)都能知足.
但事实确定有进位的运算啊!看到上面操做的不足以后,咱们确定还须要解决进位的问题对于进位的两数相加,这种核心思想为:
实现代码为:
public class Solution { public int Add(int num1,int num2) { /* * 5+3 5^3(0110) 5&3(0001) * 0101 * 0011 */ int a=num1^num2; int b=num1&num2; b=b<<1; if(b==0)return a; else { return Add(a, b); } } }
固然,这里也能够科普一下二进制求加法:average = (a&b) + ((a^b)>>1) ;
这是一道经典题,在剑指offer上也有对应题目,其具体题目要求输入一个整数,输出该数二进制表示中1的个数(其中负数用补码表示)。
对于这个问题,不用位运算将它转成二进制字符串直接枚举字符'1'的个数也能够直接求出来,可是这样作是没有灵魂的而且效率比较差。这里提供两种解决思路
法一: 你们知道每一个类型的数据它的背后实际都是二进制操做。你们知道int的数据类型的范围是(-2^31,2^31 -1)。而且int有32位。可是并不是32位所有用来表示数据。真正用来表示数据大小的也是31位。最高位用来表示正负。
首先要知道:
1<<0=1 =00000000000000000000000000000001
1<<1=2 =00000000000000000000000000000010
1<<2=4 =00000000000000000000000000000100
1<<3=8 =00000000000000000000000000001000
. . . . . .
1<<30=2^30 =01000000000000000000000000000000
1<<31=-2^31 =10000000000000000000000000000000
其次还要知道位运算&
与。两个十进制与运算.每一位同1为1。因此咱们用2的正数次幂与知道的数分别进行与运算操做。若是结果不为0,那么就说明这位为1.(前面31个都是大于0的最后一个与结果是负数可是若是该位为1那么结果确定不为0)
具体代码实现为:
public int NumberOf1(int n) { int va=0; for(int i=0;i<32;i++) { if((n&(1<<i))!=0) { va++; } } return va; }
法二是运用n&(n-1)
。n若是不为0,那么n-1
就是二进制第一个为1的变为0,后面全为1.这样的n&(n-1)
一次运算就至关于把最后一个1变成0.这样一直到运算的数为0中止计算次数就行了,以下图共进行三次运算那么n的二进制中就有三个1。
实现代码为:
public class Solution { public int NumberOf1(int n) { int count=0; while (n!=0) { n=n&(n-1); count++; } return count; } }
问题描述:
给定一个非空整数数组,除了某个元素只出现一次之外, 其他每一个元素均出现两次。找出那个只出现了一次的元素。说明:你的算法应该具备线性时间复杂度。 你能够不使用额外空间来实现吗?
分析:
这是一道简单的面试题,面试官常问怎么样用不太复杂的方法找出数组中仅出现一次的数字(其余均出现两次),暴力枚举或者使用其余的存储结构都不够优化,而这个问题最高效的答案就是使用位运算。首先你要注意两点:
具体的操做就是用0开始和数组中每一个数进行异或,获得的值和下个数进行异或,最终得到的值就是出现一次(奇数次)的值。
class Solution { public int singleNumber(int[] nums) { int value=0; for(int i=0;i<nums.length;i++) { value^=nums[i]; } return value; } }
问题描述:
给定一个非空整数数组,除了某个元素只出现一次之外, 其他每一个元素均出现了三次。找出那个只出现了一次的元素。说明:你的算法应该具备线性时间复杂度。 你能够不使用额外空间来实现吗?
分析:
这题和上一题的思路略有不一样,这题其余数字出现了3次,那么咱们若是直接使用位运算异或操做的话没法直接找到结果,就须要巧妙的运用二进制的其余特性:判断整除求余操做。即判断全部数字二进制1的总个数和0的总个数必定有一个不是三的整数倍,若是0不是三的整数倍那么就说明结果的该位二进制数字为0,同理不然为1.
在具体的操做实现上,问题中给出数组中的数据在int范围以内,那么咱们就能够在实现上能够对int的32个位每一个位进行依次判断该位1的个数求余3后是否为1,若是为1说明结果该位二进制为1能够将结果加上去。最终获得的值即为答案。
具体代码为:
class Solution { public int singleNumber(int[] nums) { int value=0; for(int i=0;i<32;i++) { int sum=0; for(int num:nums) { if(((num>>i)&1)==1) { sum++; } } if(sum%3==1) value+=(1<<i); } return value; } }
题目描述
一个整型数组里除了两个数字以外, 其余的数字都出现了两次。请写程序找出这两个只出现一次的数字。
思路:
上面的问题处理和理解起来可能比较容易,可是这个问题可能稍微复杂一点,可是这题能够经过特殊的手段转化为上面只出现一次的一个数字问题来解决,固然核心的位运算也是异或^
。
具体思路就是想办法将数组逻辑上一分为二!先异或一遍到最后获得一个数,获得的确定是a^b
(假设两个数值分别为a和b)的值。在看异或^
的属性:不一样为1,相同为0. 也就是说最终这个结果的二进制为1的位置上a和b是不相同的。而咱们能够找到这个第一个不一样的位,而后将数组中的数分红两份,该位为0的进行异或求解获得其中一个结果a,该位为1的进行异或求解获得另外一个结果b。
具体能够参考下图流程:
实现代码为:
public int[] singleNumbers(int[] nums) { int value[]=new int[2]; if(nums.length==2) return nums; int val=0;//异或求的值 for(int i=0;i<nums.length;i++) { val^=nums[i]; } int index=getFirst1(val); int num1=0,num2=0; for(int i=0;i<nums.length;i++) { if(((nums[i]>>index)&1)==0)//若是这个数第index为0 和num1异或 num1^=nums[i]; else//不然和 num2 异或 num2^=nums[i]; } value[0]=num1; value[1]=num2; return value; } private int getFirst1(int val) { int index=0; while (((val&1)==0&&index<32)) { val>>=1;// val=val/2 index++; } return index; }
固然,上面的问题可能有更好的解法,也有更多经典位运算问题将在后面概括总结,但愿本篇的位运算介绍可以让你有所收获,对位运算能有更深一点的认识。对于不少问题例如博弈问题等二进制位运算可以很巧妙的解决问题,往后也会分享相关内容,敬请期待!
原创公众号: bigsai
原创不易,若是有收获请不要吝啬你的 赞赞!
文章已收录在 全网都在关注的数据结构与算法学习仓库
我们下次再见!