位运算、原码反码补码概念、位运算常考算法题(数组中出现一次的数字、二进制表示中1的个数)

位运算、原码反码补码概念、位运算常考算法题

1.概述

位运算有6种,与、或、非、异或、左移、右移,左右移又分带符号位和不带符号位的左右移

位运算是直接在二进制层面0和1上操作的,所以它的操作效率非常高,比如正数除法可可以通过右移实现,而且它的效率比用除法高很多,java中很多类源码需要运算时常能看到作者用的是位运算来操作。

2. 异或、左移、右移

2.1 异或

异或就一句口诀,相异为1,相同为0,比如:

1 ^ 0 = 1 1 ^ 1 = 0 0 ^ 0 = 0

2.2 带符号位的左移(移一位就相当于乘一个2)

这个就麻烦了,我们先来认识一下三个小概念,分别是原码、反码、补码

带符号的类型是有符号位的,正数的符号位是0,负数是1,位于二进制表示的最左边一位

正数的原码、反码、补码都是一样的

十进制数原码反码补码
+70 1110 1110 111
+60 1100 1100 110

负数的原码、反码、补码

十进制数原码(相对于正数,只是符号位由0变1)反码(原码符号位不变,其余部分按位取反)补码(反码加1)
-71 1111 0001 001
-61 1101 0011 010

总结

  1. 正数的原码、反码、补码都一样

  2. 负数的原码就是将正数的符号位由0变成1

    负数的反码就是保持原码的符号位不动,其余部分按位取反,0变1,1变0

    负数的补码就是在反码的基础上加1

无论是正数还是负数,计算机存储的都是它的补码

十进制数计算机的存储形式是其补码32位计算机下的存储形式(负数一直补1,正数一直补0)
-710011111 1111 1111 1111 1111 1111 1111 1001
+701110000 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 带符号位左右移总结
  1. 正数的左移和右移都是直接在原码上进行移动的,不需要考虑补码
  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 00000111 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;
    }
3.3 判断一个数是否是奇数,高效的做法不是用n%2,而是让n&1,看结果是1还是0
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值