概述
从现代计算机中所有的数据二进制的形式存储在设备中。即0、1两种状态,计算机对二进制数据进行的运算(+、-、*、/)都是叫位运算,即将符号位共同参与运算的运算。
口说无凭,举一个简单的例子来看下CPU是如何进行计算的,比如这行代码:
int a = 35;int b = 47;int c = a + b;
计算两个数的和,因为在计算机中都是以二进制来进行运算,所以上面我们所给的int变量会在机器内部先转换为二进制在进行相加:
35: 0 0 1 0 0 0 1 1
47: 0 0 1 0 1 1 1 1
————————————————————
82: 0 1 0 1 0 0 1 0
所以,相比在代码中直接使用(+、-、*、/)运算符,合理的运用位运算更能显著提高代码在机器上的执行效率。
性质
1.按位与运算符(&)
参加运算的两个数据,按二进制位进行“与”运算。
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
即:两位同时为“1”,结果才为“1”,否则为0
例如:3&5 即 0000 0011& 0000 0101 = 00000001 因此,3&5的值得1。
另,负数按补码形式参加按位与运算。
“与运算”的特殊用途:
(1)清零
如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
(2)取一个数中指定位
方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。
例:设X=10101110,
取X的低4位,用 X & 0000 1111 = 00001110 即可得到;
还可用来取X的2、4、6位。
(3)判断奇偶
只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((a & 1) == 0)代替if (a % 2 == 0)来判断a是不是偶数。
2.按位或运算符(|)
参加运算的两个对象,按二进制位进行“或”运算。
运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;
即 :参加运算的两个对象只要有一个为1,其值为1。
例如:3|5即 00000011 | 0000 0101 = 00000111 因此,3|5的值得7。
另,负数按补码形式参加按位或运算。
“或运算”特殊作用:
(1)对一个数据的某些位置1。
方法:找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1。
例:将X=10100000的低4位置1 ,用X | 0000 1111 = 1010 1111即可得到。
3.异或运算符(^)
参加运算的两个数据,按二进制位进行“异或”运算。
运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;
即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
“异或运算”的特殊作用:
(1)使特定位翻转,该数对应X要翻转的对应位为1,其余位为零,此数与X对应位异或即可。
例:X=10101110,使X低4位翻转,用X ^00001111= 10100001即可得到。
(2)与0相异或,保留原值
如:X ^ 00000000 = 1010 1110。
下面重点说一下按位异或,异或其实就是不进位加法,如1+1=0,,0+0=0,1+0=1。
异或的几条性质:
1、交换律a ^ b = c===>a ^ c = b,b ^ c = a
2、结合律即(a ^ b) ^ c == a ^ (b ^ c)
3、对于任何数x,都有x ^ x = 0,x ^ 0 = x
4、自反性: a ^ b ^ b = a ^ 0 = a;
异或运算最常见于多项式除法,不过它最重要的性质还是自反性:A XOR B XOR B = A,即对给定的数A,用同样的运算因子(B)作两次异或运算后仍得到A本身。这是一个神奇的性质,利用这个性质,可以获得许多有趣的应用。 例如,所有的程序教科书都会向初学者指出,要交换两个变量的值,必须要引入一个中间变量。但如果使用异或,就可以节约一个变量的存储空间: 设有A,B两个变量,存储的值分别为a,b,则以下三行表达式将互换他们的值 表达式 (值) :
a=a^b;
b=b^a;
a=a^b;
应用举例1:
(1) 1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现
一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空
间,能否设计一个算法实现?
解法一、显然已经有人提出了一个比较精彩的解法,将所有数加起来,减去1+2+…+1000的和。
这个算法已经足够完美了,相信出题者的标准答案也就是这个算法,唯一的问题是,如果数列过大,则可能会导致溢出。
解法二、异或就没有这个问题,并且性能更好。
将所有的数全部异或,得到的结果与1 ^ 2 ^ 3 ^ … ^ 1000的结果进行异或,得到的结果就是重复数。
4.左移运算符(<<)
将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
例:a = a<< 2将a的二进制位左移2位,右补0,
左移1位后a = a *2;
若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
5.右移运算符(>>)
将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
操作数每右移一位,相当于该数除以2。
例如:a = a>> 2 将a的二进制位右移2位,
左补0 or 补1得看被移数是正还是负。
6.复合赋值运算符
位运算符与赋值运算符结合,组成新的复合赋值运算符,它们是:
运算规则:和前面讲的复合赋值运算符的运算规则相似。
7.不同长度的数据进行位运算
如果两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算。
以“与”运算为例说明如下:我们知道在C语言中long型占4个字节,int型占2个字节,如果一个long型数据与一个int型数据进行“与”运算,右端对齐后,左边不足的位依下面三种情况补足,
(1)如果整型数据为正数,左边补16个0。
(2)如果整型数据为负数,左边补16个1。
(3)如果整形数据为无符号数,左边也补16个0。
如:long a=123;int b=1;计算a& b。
如:long a=123;int b=-1;计算a& b。
如:long a=123;unsigned intb=1;计算a & b。
补充学习 链接
枚举一个无重复字符串所有子集的技巧
将该字符串转成二进制数组表示
例如: 用二进制数组表示单词中那些字母出现(1表示),那些没出现(0表示),找出该单词的所有子集。
ace 对应二进制 int origin = 21 (二进制10101),包含的子集(n代表子集,初始 n = origin)有:
ace 10101ce 10100ae 10001ac 00101e 10000c 00100a 00001遍历核心代码:n = (n - 1) & origin;// n-1 AND puzzleBit,生成一个puzzleBit的新的子集合
可以自己手动推算一下,是对的,n刚好遍历完所有情况
找map中 存在的 字符串str = ace(必含有首字母a)子集的个数
int first = 1 << (str[0] - 'a');while (n > 0) {// 遍历origin 的所有字母组合,当n=0时终止遍历// 按位都是1才为1,否则为0,即n这个组合包含origin 的首字母// 而且n这个组合在map中有值,即有单词长n这样,值累加给res[i]if ((n & first) != 0 && map[n] > 0) {res[i] += map[n];}// n-1 AND puzzleBit,生成一个puzzleBit的新的子集合n = (n - 1) & puzzleBit;}
寻找数字x二进制表示中 1的个数
为了表述简洁,下文用「一比特数」表示二进制表示中的 11 的数目。
每个 int 型的数都可以用 32 位二进制数表示,只要遍历其二进制表示的每一位即可得到 1 的数目。
利用位运算的技巧,可以在一定程度上提升计算速度。按位与运算(&)的一个性质是:对于任意整数 x,
令 x=x&(x−1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x 的「一比特数」。
另外,部分编程语言有相应的内置函数,例如 Java 的 \Integer.bitCount,C++ 的 __builtin_popcount,Go 的 bits.OnesCount 等,读者可以自行尝试。
int countOnes(int x) {int ones = 0;while (x > 0) {x &= (x - 1);ones++;}return ones;}
常用位运算公式
a >> b & 1 代表检查 a 的第 b 位是否为 1,有两种可能性 0 或者 1a += 1 << b 代表将 a 的第 b 位设置为 1 (当第 b 位为 0 的时候适用)
如不想写对第 b 位为 0 的前置判断,a += 1 << b 也可以改成a |= 1 << b
PS. 1 的二进制就是最低位为 1,其他位为 0 哦;>>运算符 优先级 高于 &运算符哦
以上两个操作在位运算中使用频率超高,建议都加深理解。
题目示例
相关位运算题目详见此篇 博客