位运算、原码反码补码概念、位运算常考算法题
1.概述
位运算有6种,与、或、非、异或、左移、右移,左右移又分带符号位和不带符号位的左右移
位运算是直接在二进制层面0和1上操作的,所以它的操作效率非常高,比如正数除法可可以通过右移实现,而且它的效率比用除法高很多,java中很多类源码需要运算时常能看到作者用的是位运算来操作。
2. 异或、左移、右移
2.1 异或
异或就一句口诀,相异为1,相同为0,比如:
1 ^ 0 = 1 1 ^ 1 = 0 0 ^ 0 = 0
2.2 带符号位的左移(移一位就相当于乘一个2)
这个就麻烦了,我们先来认识一下三个小概念,分别是原码、反码、补码
带符号的类型是有符号位的,正数的符号位是0,负数是1,位于二进制表示的最左边一位
正数的原码、反码、补码都是一样的
十进制数 | 原码 | 反码 | 补码 |
---|---|---|---|
+7 | 0 111 | 0 111 | 0 111 |
+6 | 0 110 | 0 110 | 0 110 |
负数的原码、反码、补码
十进制数 | 原码(相对于正数,只是符号位由0变1) | 反码(原码符号位不变,其余部分按位取反) | 补码(反码加1) |
---|---|---|---|
-7 | 1 111 | 1 000 | 1 001 |
-6 | 1 110 | 1 001 | 1 010 |
总结:
-
正数的原码、反码、补码都一样
-
负数的原码就是将正数的符号位由0变成1
负数的反码就是保持原码的符号位不动,其余部分按位取反,0变1,1变0
负数的补码就是在反码的基础上加1
无论是正数还是负数,计算机存储的都是它的补码
十进制数 | 计算机的存储形式是其补码 | 32位计算机下的存储形式(负数一直补1,正数一直补0) |
---|---|---|
-7 | 1001 | 1111 1111 1111 1111 1111 1111 1111 1001 |
+7 | 0111 | 0000 0000 0000 0000 0000 0000 0000 0111 |
进入正题:带符号位的左移
-
先看正数+7怎么左移
+7的原码是 0000 0000 0000 0000 0000 0000 0000 0111,比如对其左移三位(效果相当于连续乘三次2)
左移三位后原码变成0000 0000 0000 0000 0000 0000 0011 1000
再==将原码转成补码才能在计算机上存储,但是正数的原码和补码是一样的,所以不用再处理==,左移三位的效果相当于连续乘三次2,+7 变成了+56
其实平时在纸上计算时,不用写成32位的形式,因为左边好多0都是重复的,没必要写那么多0
-
再看负数 -7怎么左移
这里我就不写成32位的形式了,就随便写成写成8位吧,-7 的原码是 1000 0111,下面对其左移三位(效果相当于连续乘三次2)
所以-7左移三位后在计算机上的存储形式就是1100 1000,因为是32位的计算机所以往左边一直补1补到32位就行,变成1111 1111 1111 1111 1111 1111 1100 1000
-
总结:先用原码进行左移,完事之后,再将原码转成反码,最后将反码转成补码放在计算机上存储
2.2 带符号位的右移
-
先看正数+7怎么右移,这个就很简单了,+7的原码是0 000 0111
假如要右移三位,保持最左边的符号位不变,将最右边的三个数抛弃掉,变成0 000 0,然后再在符号位右边补三个0,变成0 000 000 0,所以+7右移三位的结果就是0,因为正数右移一位相当于除一个2,所以
第一次右移7 / 2 = 3
第二次右移3 / 2 = 1
第三次右移1 / 2 = 0
-
再看看负数-7怎么右移三位,(这个我现在还不敢肯定,但是做法的结果是对的)
先求-7的补码,-7的原码是1000 0111,反码是1111 1000,补码是1111 1001
然后将补码的符号位保持不变,将补码最右端的三位抛掉,变成1111 1
然后在符号位的右边再加上三个1,变成1 111 1111 1, 111111111在计算机中表示-1,如果是32位,那就用32个1表示,1111 1111 1111 1111 1111 1111 1111 1111(二进制 )= -1(10进制),-7右移三位结果就是-1,你可以在win10自带的计算器上实验一下
2.3 带符号位左右移总结
- 正数的左移和右移都是直接在原码上进行移动的,不需要考虑补码
- 负数左移【特殊】,先用原码进行左移,完事之后,再将原码转成反码,最后将反码转成补码放在计算机上存储
- 负数右移,直接用补码进行右移
2.4 注意!注意!注意!
假如它给的一个带符号二进制表示是1000 1010,并叫你将该二进制表示右移三位,默认给出的这个就是补码
一般给出的不是十进制的数字,而是给出的一堆带符号位的数字比如1000 1010,就默认它在计算机上就是这样存储的了,默认这就是补码。我们可以直接对补码进行移位操作
1000 1010 >> 3 = 1111 0001
0000 1010 >> 2 = 0000 0010
2.5 无符号数的左移右移
比如32位的计算机,如果有符号那么就要在32位中取出1位来存储正负,就是最左边的一位,int类型占4个字节,也就是32位,带符号的int有正负之分,范围就是
进制表示 | 有符号int类型的最小值 | 有符号int类型的最大值 |
---|---|---|
十进制 | -2 147 483 648(大概是-21亿) | 2 147 483 647(大概是21亿) |
二进制 | 1000 0000 0000 0000 00000000 0000 0000 | 0111 1111 1111 1111 1111 1111 1111 1111 |
最大值 + 1 = 最小值, 最大值和最小值只是一念之间
因为有了符号,我们左移右移时需要考虑很多,但是无符号的int类型没有负数这个概念,32位数全用来表示大小了,所以最小是0,最大能达到42亿,左移右移时就当做是一个正数直接移动就行,而且这个正数还没符号位
比如7,二进制表示为0000 0111,右移一位就是0000 0011,右移两位就是0000 0001,右移三位0000 0000
比如8,二进制表示为0000 1000,左移一位就是0001 0000,左移两位就是0010 0000,左移三位0100 0000
3. 常考算法题(重点)
3.1 二进制中1的个数
题目:请实现一个函数,输入一个整数,输出该数二进制表示中的1的个数,例如把9表示成二进制是1001,有2位是1,因此如果输入是9,该函数输出是2
分析:先判断整数二进制表示中的最右边一位是不是1,接着把输入的整数右移一位,此时原来处于从右边数起的第二位被移到了最右端,再判断此时的最右端是不是1。这样每次移动一位,直到整个整数变成0为止
代码1:
private static int numberOf1(int n) {
//计算n的二进制表示中1的个数
int count = 0;
while (n != 0
//1.将n的二进制表示中的最右端一位与1相与,如果结果是1,count++
if((n & 1) == 1){
/*
比如n是5,那就是0101
0 1 0 1
& 0 0 0 1
-----------------
0 0 0 1
*/
count++;
}
//2.每次做完与运算,就将n右移一位,刚才是0101,那应该要变成0010
n = n >> 1;
}
return count;
}
反思:这个代码是有问题的,因为如果输入的是负数,负数在右移的同时,会在符号位的右端补1,最终会导致整个二进制表示成-1,比如-5的原码是1000 0101,反码是1111 1010,补码是1111 1011,那么它在8位计算机中的表示就是补码的形式1111 1011
1111 1011 >> 1 = 1111 1101
1111 1011 >> 2 = 1111 1110
1111 1011 >> 3 = 1111 1111
-5一旦右移三次就会变成全1的局面,那么while(n != 0)就会永远为true,陷入死循环
代码2:我们可以试着让输入的n不移位,让1左移位,然后再相与,看代码会清晰点
private static int numberOf1(int n) {
int count = 0;
//n不移位,让flag负责移位
int flag = 1;
//因为n不移位,所以值不改变,flag左移位,值会改变,最终变成0
while(flag != 0){
if((flag & n) != 0){
/*
比如n是-5,32位计算机下是1111 1111 1111 1111 1111 1111 1111 1111 1011
1111 1111 1111 1111 1111 1111 1111 1111 1011
& 0000 0000 0000 0000 0000 0000 0000 0000 0001
-----------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0000 0001
*/
count++;
}
//flag左移一位,变成0000 0000 0000 0000 0000 0000 0000 0000 0010,循环32次,一直左移动32次,flag最终变成0
flag = flag << 1;
}
return count;
}
以上的代码2虽然解决了负数的问题,但是毕竟要循环32次,属于O(n)的解法,我们还有种最为高效的做法,可以使得有多少个1就执行多少次,比如5是0101,我们只需执行两次就能得到结果,相比于代码2中的32次,这个做法更能让人眼前一亮
这个方法的大意就是:把一个整数减去1之后再和原来的整数做位与运算,比如这个整数是5,5 - 1 = 4,然后再用4和5做位与运算,4是0100,5是0101
0 1 0 0
& 0 1 0 1
0 1 0 0
我们会发现结果0 1 0 0 和输入的0 1 0 1相比差一个1(这个1右边数起的第一个1),也就是说把一个整数减一再和原来的整数做位与运算,得到的结果相当于是把整数的二进制表示中的最右边的一个1变成0。
我们可以接着再让4 - 1 = 3,让3和4相与,3是0011,4是0100
0 0 1 1
& 0 1 0 0
0 0 0 0
这时候又一个1变成0,经过这两次操作,输入的整数5的二进制表示中的全部1都已经被消化掉了,我们只需判断n经过多少次这样的操作变成0,就说明它里面有多少个1,结合下面代码思考
代码3(非常重要的一个方法,要理解透并能随手写出代码)
private static int numberOf1(int n) {
int count = 0;
while (n != 0){
//只要一个数不是等于0,不管它是正数或是负数,它的二进制表示中一定存在1,count加1
count++;
//把一个整数减1再与原来的整数相与,得到的结果就是原来的整数的二进制表示中的最右端的1变成0
/*
比如整数是5,5 - 1 = 4,5的二进制表示为0101,4的二进制表示为0100,5和4相与
0 1 0 1
& 0 1 0 0
——————-----
0 1 0 0 这个结果就是比0101少一个1
*/
n = (n - 1) & n;
}
return count;
}
3.2 数组中只出现一次的数字(某一个数字出现奇数次,其他数字均出现偶数次)
数组中某一个数字只出现奇数次,其他数字均出现偶数次,找出该数字
在这题中,可能异或技巧是最为犀利的一种解法,因为异或是相异为1,相同为0,所以相同的数字异或结果一定是0,我们只需把数组从头异或到尾,最终的结果一定是那个出现奇数次的数字
private static int singleNumber(int[] nums)
{
int ret=0;
for(int i = 0; i < nums.length; i++)
{
ret ^= nums[i];
}
return ret;
}