位运算的运用场景使用总结

位运算的使用场景总结

前言

本文总结了位运算在算法、源码、面试中和Android中的运用场景使用。不足之处,欢迎指正。

位运算相关概念

什么是位运算 ?

计算机在底层使用的是二进制补码进行运算。对应的二进制位进行操作,计算机只识别0和1。

位运算的好处

巧妙的使用位运算可以大量减少运行开销,优化算法。

位运算快的原因是直接跟计算机的底层二进制机器操作指令,而我们的程序代码运算最终也是由 JVM转换成计算机可执行的二进制操作指令,位运算省略了中间转换的操作,处理器直接操作所以更快。

bit 和byte

  1. bit指“位‘,是数据传输速度的计量单位,常简写为“b”;Byte指“字节”,是文件大小的计量单位,常简写为“B”。

  2. Byte和bit的换算关系是,1 Byte=8 bits。在电脑上,一个英文字母需要占用1 Byte的硬盘空间,一个汉字则需占用2 Byte。

    例如,在我们java语言中,一个int 占4 byte,也就是占32bit。

原码

将一个数字转换成二进制(机器数)就是这个数值。

反码

反码的表示方法是:正数的反码是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。

补码

补码的表示方法是:正数的补码就是其本身;负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1。 (即在反码的基础上+1)

十进制原数 原码 反码 补码
10 0000 1010 0000 1010 0000 1010
-10 1000 1010 1111 0101 1111 0110
5 0000 0101 0000 0101 0000 0101
-5 1000 0101 1111 1010 1111 1011

设计意义

简化了计算机的设计,计算机只能进行加法运算,通过补码的设计,使之可以在这种设计下,进行减法运算。
比如 1-1 在计算机中执行的 实际上是 1 +(-1) 即补码运算,也就是说,所有计算都是使用该数的补码,计算完成以后再换回源码。

位运算基础

  1. 位运算是针对整数的二进制进行的位移操作

  2. 整数 32位(4个字节) , 正数符号为0,负数符号为1。十进制转二进制 不足32位的,最高位补符号位,其余补零

  3. 在Java中,整数的二进制是以补码的形式存在的

  4. 位运算计算完,还是补码的形式,要转成原码,再得出十进制值

  5. 正数:原码 = 反码=补码 负数:反码=原码忽略符号位取反, 补码=反码+1

例如:十进制4 转二进制在计算机中表示为(补码) 00000000 00000000 00000000 00000100

例如:十进制-4 转二进制在计算机中表示为(补码) 11111111 11111111 11111111 11111100

Java支持的7个位运算符

  1. &:与运算符
  2. |:或运算符
  3. ~:非运算符
  4. ^: 异或运算符
  5. >>:右移运算符
  6. <<:左运算符
  7. >>>:无符号右运算符

运算符如何使用

位移操作:(只针对int类型的有效,Java中,一个int的长度始终是32位,也就是4个字节,它操作的都是该整数的二进制数).也可作用于以下类型,即 byte,short,char,long(它们都是整数形式)。当为这四种类型时,JVM先把它们转换成int型再进行操作。

  1. 与(&)运算符,两个都为1时才为1,其他情况均为0。 例如:-5 & 4 =0

    -5的二进制形式为:11111111 11111111 11111111 11111011

    4的二进制形式为:00000000 00000000 00000000 00000100

    进行逻辑运算后为:00000000 00000000 00000000 00000000

    转换为十进制为:0

  2. 或(|)运算符,两个都为0时才为0,其他情况均为1。 例如:5 | 4 = -1

  3. 非(~)运算符,取反,即1变为0,0变为1。 ~(-5)=4

  4. 异或(^)运算符,相同值为0,不同值为1。 -5^4 = -1

  5. 右移(>>)运算符,m>>n,把m的二进制数右移n位,m为正数,高位全部补0,m为负数,高位全部补1。例如:5>>2 =1

  6. 左移(<<)运算符,m<<n,把m的二进制数左移n位,高位超出n位都舍弃,低位补0(此时可能出现正数变负数。 5<<2 =20

  7. 无符号右移(>>>)运算符,m>>>n,整数m表示的二进制右移n位,不论正负数,高位都补0

基础介绍就到这,下面介绍位运算的使用场景。考察内功的时候到了。

使用场景

1.判断奇偶数

常规做法:

   if( n % 2) == 1
       // n 是奇数
   }

采用(&)与运算,效率直接提高。

if(n & 1 == 1){   //(n&1 == 0)//n是偶数
    //n是奇数
}

原理分析:与(&)运算 两个都为1时才为1,其他情况均为0。

2. 交换两个数

常规的使用是引入额外的变量来完成交换。

   private void swap(int x, int y) {
        int temp = x;
        x = y;
        y = temp;
    }

在面试中这个经常会做为考点,来考察内功。比如:不允许你使用额外的辅助变量来完成交换呢?

于是,用异或来完成。

  private void swap(int x, int y) {
	x = x ^ y   // (1)
	y = x ^ y   // (2)
	x = x ^ y   // (3)
    }
    
    //或者
   private void swap2(int x, int y) {
        x ^= y;
        y ^= x;
        x ^= y;
    }

原理分析:异或(^)运算符,相同值为0,不同值为1

把(1)中的 x 带入 (2)中的 x,有:

y = x^y = (x^)^y = x^(y^y) = x^0 = x。 x 的值成功赋给了 y。

对于(3),推导如下:

x = x^y = (x^y)^x = (x^x)^y = 0^y = y

3.找出没有重复的数

给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。

这道题可能很多人会用一个哈希表来存储,每次存储的时候,记录 某个数出现的次数,最后再遍历哈希表,看看哪个数只出现了一次。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)了。

然而我想告诉你的是,采用位运算来做,绝对高逼格!

两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下,

例如这组数据是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出现了一次,其他都出现了两次,把他们全部异或一下,结果如下:

由于异或支持交换律和结合律,所以:

1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5

int find(int[] arr){
    int tmp = arr[0];
    for(int i = 1;i < arr.length; i++){
        tmp = tmp ^ arr[i];
    }
    return tmp;
}

时间复杂度为 O(n),空间复杂度为 O(1),而且看起来很牛逼。

4. m的n次方

如果让你求解 m 的 n 次方,并且不能使用系统自带的 pow 函数(Math.pow(底数,几次方)),你会怎么做呢?

思路1:

int pow(int n){
    int tmp = 1;
    for(int i = 1; i <= n; i++) {
        tmp = tmp * m;
    }
    return tmp;
}

时间复杂度为 O(n)

我举个例子吧,例如 n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为:

m^1101 = m^0001 * m^0100 * m^1000。

我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:

int pow(int n){
    int sum = 1;
    int tmp = m;
    while(n != 0){
        if(n & 1 == 1){
            sum *= tmp;
        }
        tmp *= tmp;
        n = n >> 1;
    }

    return sum;
}

时间复杂度近为 O(logn),而且看起来很牛逼。

5.求绝对值

    private int abs(int x) {
        int y;
        y = x >> 31;
//        return (x ^ y) - y;
        return (x +y)^y;
    }

等价于:Math.abs()

6.取模运算

a % (2^n) 等价于 a & (2^n - 1)

System.out.println(4 & (2 ^ 2 - 1));//结果为0
System.out.println(4 % 4); //结果为0

7. 乘法运算

​ a * (2^n) 等价于 a << n

8.除法运算转化成位运算

​ a / (2^n) 等价于 a>> n

System.out.println(4 << 2); //4乘以2^2 = 4X4=16
System.out.println(4 >> 2); //4除以2^2 =4除以4=1

9.求相反数

​ (~x+1)

 System.out.println(~7 + 1); //结果为:-7

10. HashMap中哈希算法的使用

//重新计算哈希值
static final int hash(Object key) {
    int h;
    //key如果是null 新hashcode是0 否则 计算新的hashcode
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • ^按位异或运算,只要位不同结果为1,不然结果为0;
  • >>> 无符号右移:右边补0

11.经典面试题

问题:有1000个一模一样的瓶子,其中有999瓶是普通的水,有一瓶是毒药。任何喝下毒药的生物都会在一星期之后死亡。现在有一些老鼠(无穷),给你一个星期时间,问最少需要几只老鼠可以找出这瓶有毒药的水?

解答:2 ^ 10 = 1024 > 1000,所以,最少需要10个老鼠。
将1-1000表示为二进制形式:
1234 5678
1 0000 0001
2 0000 0010
3 0000 0011
4 0000 0100
5 0000 0101
6 0000 0110
7 0000 0111
8 0000 1000
9 0000 1001
10 0000 1010
11 0000 1011
12 0000 1100
13 0000 1101
14 0000 1110
15 0000 1111
……
如上所示:1号老鼠喝第一位为1的水
2号老鼠喝第二位为1的水
3号老鼠喝第三位为1的水
4号老鼠喝第四位为1的水
5号老鼠喝第五位为1的水
6号老鼠喝第六位为1的水
7号老鼠喝第七位为1的水
8号老鼠喝第八位为1的水
9号老鼠喝第九位为1的水
10号老鼠喝第十位为1的水
如果i号老鼠死了,则第i位为1。否则,为0。最后得到的数字即为所求。

扩展:如果你有两个星期的时间(换句话说你可以做两轮实验),为了从1000个瓶子中找出毒药,你最少需要几只老鼠?注意,在第一轮实验中死掉的老鼠,就无法继续参与第二轮实验了。
提示:用三进制数表示即可。

12.Android中项目中使用场景

MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSizeSpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。(引用自《Android开发艺术探索》)

在这个类里面,一开始就声明了两个基本变量以及不同测量模式的值,已经用到了位运算中的左移。

private static final int MODE_SHIFT = 30;
// 声明位移量
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
// 后期截取SpecMode或SpecSize时使用的变量
// 3对应的二进制是11,左移30位后,int值的前2位就都是1,后30位为0
 
public staic final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

// 三种测量模式对应的值
//获取 MeasureSpec
    public static int makeMeasureSpec(int size, int mode) {
        // API 17 之前,忽略此条件
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
     }
 
    //获取 SpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
 
    //获取 SpecSize
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

原理分析:

在 MeasureSpec 类中,getMode 方法是将参数 measureSpec 与 MODE_MASK 进行与运算,MODE_MASK 可以理解为 SpecMode 的掩码,运算的结果是保留measureSpec 的高两位,剩下的后 30 位置 0,得到的是 MeasureMode。

getSize 方法是先将 MODE_MASK 取反再跟 measureSpec 进行与运算,结果是高两位为 0 低 30 位不变的值,即 SpecSize。

makeMeasureSpec 方法中,size & ~MODE_MASK 的结果是 size 的 SpecSize,mode & MODE_MASK 的结果是 SpecMode,将他们进行或操作,得到的就是是两者的叠加值。

其他运用场景,后期继续完善~

参考:

Java中的位运算

位运算装逼指南

HashMap中的hash算法中的几个疑问

位运算在Android中的使用场景

谈谈位运算和在Android中的运用

经典面试题位运算的使用

猜你喜欢

转载自blog.csdn.net/jun5753/article/details/107514949