基本的位运算操作

1,113 阅读6分钟

原码,反码,补码

从二进制说起

为了简单点儿,我们只使用四位0和1。 对于四位0和1,我们能表示的正数范围为0000 ~ 1111(015); 如果最高位用作符号位,0表示正数,1表示负数,则能表示的数字范围为(-77); 首先来看最简单的正数情况,如果为正数,那么原码,反码,补码规定为一致的; 所以我们下面的讨论,主要是针对有符号位的负数。 例如,对于-1:

原码反码补码
100111101111
对于获取反码的处理,我们只需把非符号位反转,1变0,0变1;
对于补码的获取,则是在反码的基础上+1;
对于反码和补码的出现,我们可以这样理解,计算机利用反码和补码,将负数在计算机中以补码的形式表示,把所有计算都变成了加法运算,这样,对计算机来来说,除了位移操作,就是加法操作,对于计算机来说是简单了。

位操作

取反

例如,0011,取反变成 1100,就是简单的1变成0,0变成1操作。

例如,0011 与 1010 按位或变成 1011。 0和1进行或运行,只要两个操作数中有1,结果就为1,或两个0时候才为0.

例如,0011 与 1010 按位与变成 0010 。这个操作看起来严格些,只有当两个1出现时候结果才为1,其他情况均为0.

在进行与操作的时候,我们通常会利用 0 & 1 = 0 来对数据的一些位进行清零操作,这里的1通常是左移或者右移N位之后的1。

异或

例如,0011 与 1010 按位异或变成 1011 。 异或,就是长得不一样时候,即一个0一个1时候结果才为1.让我想起B站上经常重金求1了,毕竟,一样的话,就没什么搞头了。 异或操作有这样的特性:

  • 0 ^ N = N
  • N ^ N = 0
  • a ^ b = b ^ a
  • a ^ b ^ c =(a ^ b) ^ c = a ^ (b ^ c)

前面三个特性很好理解,最后一个我们来看一个例子,不使用中间变量进行的两数交换:

a = a ^ b;
b = a ^ b;
a = a ^ b;

最早看到这段祖传代码,还是在如何实现两数交换的问题里面,我们根据最后一个特性,来分析下这个是如何实现两数交换的:

  • 首先令 a = a ^ b,之后 b = a ^ b 相当于 b = a ^ b ^ b,此时b的值为原始a的值;
  • 之后,a = a ^ b,此时,a = a(原始) ^ b(原始),则 a(原始) ^ b(原始) ^ a(原始),则最后一步中,a的新值变为b的原始值。

另外,整体上看,异或是个很均匀的操作,因为出现结果为0或者为1的概率,都是百分之五十,不像上面的或操作,结果出现1的概率大一些。

左移右移

例如,0011 左移1位 ,变为 :0110 。对于左移动右移,空出来的前后都补0.

另外,需要注意下有符号的移动和无符号的移动,例如:

/**有符号右移>> : 左边的补上符号位,正数补0,负数补1**/
System.out.println(-8 >> 2); // -2
/**有无符号右移>>> :无论该数为正还是为负,右移之后左边都是补上0 **/
System.out.println(-8 >>> 2); //1073741822

对于有符号的移动,可以理解成带着符号的乘除2的N次;对于无符号右移动,可以理解为单纯的对0和1的移动。

Java中一些常见的位运算操作

使用二进制中的位代表true or false

例如,求解一个不同元素数组的子集,例如:给定数组[1,2,3],它的子集为:[],[1,2,3],[1],[2],[3],[1,2],[1,3],[2,3] 。 我们使用0表示当前数字不在,1表示当前数字在:

123
000
001
010
100
011
101
110
111

根据上面枚举,可以观察到,对于N个元素,存在 2^n 个子集。我们求解子集只需列举出所有可能的0和1的组合。

  public List<List<Integer>> subsets1(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int length = 1 << nums.length; //子集的总个数
        for (int i = 0; i < length; i++) {
            List<Integer> list = new ArrayList<>();
            for (int j = 0; j < nums.length; j++) {
                //如果当前的i值的二进制位跟当前数组所在 nums 的index相与不为1,则说明当前num[j]在此子集中
                if (((1 << j) & i) != 0) {
                    list.add(nums[j]);
                }
            }
            res.add(list);
        }
        return res;
    }

或操作符示例

数组转换为整数

public int consoleIntegerFromByte(int[] source) {
       int res = 0;
       for (int i = 0; i < 32; i++) {
           res = source[i] << (31 - i) | res;
       }
       return res;
   }

例如,我们现在有个二进制串串:0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1。

想要获取它的十进制数值,可以进行相反的操作,注意这里的或操作,利用这个或操作,我们能将每一位移动到它该在的位置上,并且保留之前位移动的结果。

总结下这里:我们为了获取保留之前二进制位的操作结果,通常使用或,在操作过程中将某一位的操作结果融合进入之前的结果里面。

与操作示例

逐位输出整数的二进制位

public int[] consoleIntegerByte(int num) {
       int[] result = new int[32];
       for (int i = 31; i >= 0; i--) {
           result[31 - i] = num >> i & 1;
       }
       return result;
   }

例如,对于正数1,二进制表示为:0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 。

我们采取移位操作,例如,先将最高位的0向右边移动31位,然后与1进行相与操作,这里与1进行相与操作,是为了保留最低位的值。

依次对原数的每位进行移动到最低位与1相与,我们即可得到最终的结果。 这里,我们还可以用这个方法,输出下-1的所有二进制位:1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 。有木有发现,输出的,是-1的补码。忘记的话,知识点,往上翻。

总结下这里:我们为了获取某一位上面的二进制位,通常选择与1相与。

异或操作示例

用在hashCode对数据进行均匀分散

public static int hashCode(long value) {
       return (int)(value ^ (value >>> 32));
   }

这是long类型数值获取hash code 的方法,注意这里这个异或,将64位的01的高位和低位进行异或,均匀分散01. PS:注意这里的右移操作,为无符号右移,如果用有符号右移,对于负数的补1操作,就不能保证在所有数据中,0和1出现的概率一样了。

利用异或进行反操作

再来看异或的另一个比较秀的用法:

/**
    * 将字符大写变小写,小写变大写
    *
    * @param c
    * @return
    */
   public char changeWordToOtherCase(char c) {
       c ^= (1 << 5);
       return c;
   }

由于大小写字母相差32,这里异或 1<<5,相当于加减32。

体会下,对于大写A(0100 0001) 异或上一个左移五位的1,之后会变成0110 0001,相当于值加上32,变为a(0110 0001);对于小写的a,那个1将会被和谐掉。

相当于根据当前传入值的第6位上的1进行了一个加减32的操作。秀~~

取反及位移操作

求解一个整数的补数

例如,对于数字5(101),补数为 2(10),即把有效位1变0,0变1,听起来很像是取反的操作,但是又不是完全的取反.

因为在计算机中,32位的5,除了数值位101,前面还有填充的0,如果只是~5,并不能得到10,这里我们需要的是一个针对有效位的取反。

public int findComplement(int num) {
       int tmp = num, bit = 0;
       //取的有效位个数操作
       while (tmp > 0) {
           tmp >>= 1;
           if (bit == 0)
               bit = 1;
           else
               bit = bit << 1;
       }
       //如果num的有效位为N,此时bit值为 1 << (N+1)
       return ~num & (bit - 1);
   }

我们来分析下这个代码,为了取反,首先要取的num的有效位个数,这里对num从低位到高位右移,直到num为0为止。bit在num的操作中,反之每次向左移1,这样,当循环结束时候,如果num的有效位为N,此时bit值为 1 << (N+1),此时如果bit - 1,则bit的低 N 位全为1 ,之前我们提过, 为了获取某一位上面的二进制位,通常选择与1相与, 此时 ~num & (bit - 1) ,则可获得低位的有效N位。

总结,为了获取N个1,我们常常采用 1 << (N+1) - 1的方法,之后利用这N个1进行某些特殊连续位的值保留。

相对于逻辑门之类的操作,取反或者位移操作,一般相当于团战中的辅助,属于对具体位置上变动。通常是采用这两个操作对单个位上的数字进行移动或变动,之后再用其他操作进行整体上的操作。