位运算
位运算是将整数或字符转化为2进制,对2进制的每一位进行运算。
由于计算机在进行十进制运算时,都要将十进制转化为二进制再进行计算。那么如果一开始就用二进制计算也就是位运算,那么效率将会比十进制转化为二进制再进行运算要高得多,也就是说,这是一个十进制运算的底层算法,省去了将十进制转化为二进制的时间,以提高效率。
移位
左移
x<<n; //这表示将操作数x按二进制向左移动n位,右边新生成的的空位补0
类比十进制乘法.事实上,这相当于
但是这其中会遇到溢出的问题(移动的位数过多),但是Python不会(Python是最好的编程语言!!!),因为Python会在溢出后自动选择更大的数据类型。
右移
算数右移(仅有Java支持)
x>>n; //将操作数x按照按进制向右移动n位,左边按照原来的最高位来补
事实上,类比十进制除法和左移。这相当于
由于前面按照最高位补,则不会改变原来数字的符号。这保证了除法的可行。这引出了下面的另一种右移:无符号右移。
逻辑右移(C/C++对于无符号数均为逻辑右移,对于有符号数取决于编译器)
x>>>n; //将操作数x按二进制向右移动n位,左边补0
移动过后都变成正数了。
按位与(&)
相当于逻辑且,例如:3&6
用途
清零(把开关弹起来)
只要符合原数的1位对应为0,进行与操作后就可以全部变为0。当然直接用全为0的数进行与运算更容易。例如:
取出特定位数的数
由于按位与如果是1&1=1,如果是0&1=0,这相当于如果用1来与(&)原数对应的位,那么对于不改变原数;用0与,则不管是什么都会变成0。也就是说,如果我们有两个数(以整数为例)a,b.想要取出a中特定位数的数,使用b作为算子。那么我们只需要将b在我们需要取出的数位赋为1,其余均为0即可,例如:
代码实现:
int GetBit(int a, int i)
{
//这个GetBit函数用来返回a的二进制表示中的从右向左第i+1位的二进制数。这只需要用到与运算
//由于i<<i扩大了2^i倍,最后还要再除以2^i
return ((1<<i) &a)>>i ;
}
上述是使i的第i+1位变成1去与a,也可以反过来让a把最后一位变成第i+1位的二进制数与1
int GetBit(int a,int i)
{
return (a>>i)&1;
}
按位或(|)
相当于逻辑或,例如3|6
应用
设置某位为1(把开关按下去)
由于1|1=1,1|0=1,0|0=0.所以一个数|1,那么结果一定是1;一个数|0,则不改变原数.利用这一特点,可以设计使用按位或对于某数的特定位数设置为1,要达到这一目标,只要将我们想要设置为1的数位设为1,以构造b即可.
例如:
代码实现:
int SetBit(int a,int i,int j)
{
if(j==1) //将a的从右至左第i+1位设置为1.
return a|(1<<i);
else if(j==0) //将a的从右至左第i+1位设置为0.
return ~a&(1<<i);
return -1;
}
按位非(~)
略。本质是反码。
注意:对于符号位同样也会取反
代码实现:
int Flip(int a)
{
//想要反转,只要使用非运算即可.
return ~a;
}
按位异或(^)
相同为0,不同为1,等价于不进位的加法,可以区分两个数字是否相同。例如:3^6
应用
反转特定位(按开关)——强化版的非运算
如果想要将1变为0,那么只要使两个数字相同,也就是用1^1=1;想要将0变为1,只要使两个数字不同,也就是0^1=1。那么只要用1异或一个数,无论如何都可以起到反转这个数位的作用。
例如:
进一步地,我们可以知道非运算等价于a与一个 每个数位都是1 的数进行异或。
交换两数
a=a^b;
b=b^a;
a=a^b; //这样就可以实现不用中间变量实现交换两数。然而这样比较复杂。且不容易debug,不容易让别的程序员看懂以至于容易挨揍。
这相当于
a=(a^b)^(b^(a^b))=a^b^b^a^b=a^a^b^b^b=b//这里有一个疑问:为什么或运算满足交换律?
消去二进制中最右侧的1
可以用x&(x-1)来消去x的最后一位的1.这是因为(x-1)如果转化为二进制就相当于将x的最右边一个1变为0
例如:
这个应用可以找到二进制表达式中最右侧的1并消去.
不同长度的数据进行位运算
按照较长数据的长度,将较短数据“右移”至与较长数据长度相同。其实本质上还是在不改变符号的前提下扩展数据。
复合的位运算应用
位运算的公式们
-x = ~x + 1 = ~(x-1)
~x = -x-1
-(~x) = x+1
~(-x) = x-1
x+y = x - ~y - 1 = (x|y)+(x&y)
x-y = x + ~y + 1 = (x|~y)-(~x&y)
x^y = (x|y)-(x&y)
x|y = (x&~y)+y
x&y = (~x|y)-~x
x==y: ~(x-y|y-x)
x!=y: x-y|y-x
x< y: (x-y)^((x^y)&((x-y)^x))
x<=y: (x|~y)&((x^y)|~(y-x))
x< y: (~x&y)|((~x|y)&(x-y))//无符号x,y比较
x<=y: (~x|y)&((x^y)|~(y-x))//无符号x,y比较
给出两个32位的整数N和M,以及两个二进制位的位置i和j。写一个方法来使得N中的第i到j位等于M(M会是N中从第i为开始到第j位的子串)
这是一道不值一提的题目,只不过是前面两种技巧的复合而已。略。
给出两个整数a和b, 求他们的和, 但不能使用 + 等数学运算符。
这道题是一道好题,与数学结合得比较紧密。我们知道异或(^)运算是不进位加法,现在要考虑进位加法,该怎么办?
首先考虑如何判断哪一位需要进位?在二进制中,如果两个数的同一位都是1,那么就需要进位了,例如:
那么a+b的第三位就需要进位。那么 如何判断是否进位 这个问题就转换为了 如何判断两个数的同一位都是1?显然,这就是与运算的定义。
我们判断了是否需要进位,接下来,我们该怎么表示进位呢?进位,实际上就是将这一位变为0,然后将下一位变为1。颇有一点乘法的味道,没错,这时需要用到左移运算(乘法)。即当a&b=1时,将a&b左移一位(1×2)。当a&b=0时,不左移。事实上,不左移与0左移1位(0×2=0)是一样的,那么上述分类讨论就可以合并为:
(a&b)<<1 //表示进位
表示进位后,我们就可以将进位与不进位结合起来,得到最终的结果:
a+b=(a^b)+(a&b<<1) //对于每一位来讲的最终结果
可以使用循环来对每一位都进行上述操作,最终就可以解决了。这里直接贴一段九章算法网站的Java代码:
class Solution {
/*
* param a: The first integer
* param b: The second integer
* return: The sum of a and b
*/
public int aplusb(int a, int b) {
while (b != 0) {
int _a = a ^ b;
int _b = (a & b) << 1;
a = _a;
b = _b;
}
return a;
}
};
用O(1)时间检测整数是否是2的正整数次幂
若
则N必满足:
1.N>0
2.N的二进制表示中只有一个1,这是因为其一定可以写为1(其中这个1位于右向左第n+1位)后面全是0的形式(由于2进制的定义)
那么如果将N的二进制表达中唯一的1消去,应该返回0.利用这一特点我们立刻可以写出时间复杂度位O(1)的代码,还是直接粘贴了……
class Solution {
public:
/*
* @param n: An integer
* @return: True or false
*/
bool checkPowerOf2(int n) {
// write your code here
return n > 0 && (n & (n - 1)) == 0;
}
};
统计二进制表达式中有多少个1
这是一道不足挂齿的题目,略。
如果将整数A变为B,需要改变多少个bit位?
只需要考虑A与B有多少位不同即可。只要使用异或(相同为0,不同为1)即可,略。
枚举子集
由于二进制只具有两种状态,这个"解空间"与布尔型的"取或不取"的"解空间"同构。那么我们可以用二进制的第i位表示有序集的第i位取出或者不取出。
题目待补充。
找出只出现奇数次的数
由于a^b^b=a,那么我们只要将所有的数异或两次,找变化的即可,略。
数组中,只有一个数出现一次,剩下都出现三次,找出出现一次的数
待补充。
参考资料
1.黄程程讲编程 Java017 位运算:移位、按位与、或、非、异或
2.位运算
3.九章算法-位运算