小言_互联网的博客

盘点那些能绿天绿地绿巨人的位运算操作

253人阅读  评论(0)

假装体现主题的标题

这篇文章的内容是我从 剑指Offer对答如流系列 - 二进制中 1 的个数 中抽出来的,唉。写个位运算的解析,写入迷了,对位运算的讨论有点多了,干脆拿出来单独成一片文章算了。

一、盘点位运算的骚操作

位运算 效率高,速度快,是开车老司机出行游玩,登山必备。

前三条的运用参考 剑指Offer对答如流系列 - 二进制中 1 的个数 ,下面出发吧。

(1)移位运算(左移or右移)快于乘除

如果只针对2 进行乘除 强烈建议采用这种方法。

(2)一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0

判断一个数是否是2的次幂 也可以用这种方法呀。 n&(n-1)为0 说明这个数就是2的次幂

(3)通过右移判断二进制某位是0还是1

剑指offer的拿到面试题体现的思想与这个一致,不过是用1进行左移实现的,思想保持一致。

(4)判断奇偶数

二进制中,末位是0→偶数,末位是1→奇数。
n&1=0 — 偶数
n&1=1 — 奇数
求求你,看完这篇文章后,判断奇偶不要再取余了

(5)两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身

如果给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出这只出现一次的数 。直接一个for循环,所有的值进行异或,结果就是那个只出现一次的数。

(6)两个数的交换

高逼格啊,相信你会在某些框架的源码中看到的

number1 = number1 ^ number2;
number2 = number1 ^ number2;
number1 = number1 ^ number2;

(7)不用判断语句,求整数的绝对值

正负通用 : i^(i>>31) + i>>>31
负数专用 : i^-1 + 1
相信我 这个看框架源码 会遇见的。

有读者提问这里不太明白,我这里说一下原理:

  1. >>是带符号的右移 >>>是不带符号的右移(也叫逻辑右移)
  2. 将一个整型整数x,带符号右移31位,则结果要么是0,要么是-1。如果是0,则x为正数,为-1则x为负数。

假定x右移31位后的结果是y吧,下面我们将x与y作异或运算,考虑到上面的原理:

  • 如果y的值为0,x的值为正数,x^y的值为x本身。 x >>> 31 的值为0(正数二进制最高位为0) 它们相加的结果仍是x
  • 如果y的值为-1, x的值就是负数,x^y的值为 x的反码。比如-11的反码是10,这个时候x>>>31的值为1(负数二进制最高位为1),它们相加的结果是11,说白了就是求补码。
       public static void main(String[] args) {
            int i=-11,j=11;
            // 正负通用
            System.out.println(i+"的绝对值是"+((i^(i>>31))+(i>>>31)));
            System.out.println(j+"的绝对值是"+((j^(j>>31))+(j>>>31)));
            //已知i是负数的用法
            System.out.println(i+"的绝对值是"+((i^-1)+1));
        }

二、位运算高阶操作

上面举的用起来已经很欢乐了。要不要见一下一个大厂的专门考察位运算的面试题?

不用 + - * / 实现 加减乘除

(一)加法

对于位运算,还有一些你需要了解的基本性质

(1)二进制位异或运算相当于对应位相加,不考虑进位

1 ^ 1 = 0 ---> 1 + 1 = 0 (当前位值为0,进一位)
1 ^ 0 = 1 ---> 1 + 0 = 1 (当前位值为1)
0 ^ 0 = 0 ---> 0 + 0 = 0 (当前位值为0)

(2)二进制位与运算相当于对应位相加之后的进位

1 & 1 = 1 ---> 1 + 1 = 0 (当前位的值进一位)
1 & 0 = 0 ---> 1 + 0 = 1 (当前位的值不进位)
0 & 0 = 0 ---> 0 + 0 = 0 (当前位的值不进位)

(3)两个数相加:对应二进制位相加的结果 + 进位的结果

3 + 2 --> 0011 + 0010 --> 0011 ^ 0010 + ((0011 & 0010) << 1) ---> (0011 ^ 0010) ^ ((0011 & 0010) << 1)
当进位之后的结果为0时,相加结束

实现如下:

  int add(int a,int b){
        int s;
        int c;
        while(b != 0){
            // 获得求和位
            s = (a^b);
            // 获得进位,然后需要向做移动一位表示进位
            c = ((a&b)<<1);
            a = s;
            b = c;
        }
        return a;
    }

换成递归的写法会更清晰点

    int add(int a,int b){
        // 假如进位为0
        if(b ==0){
            return a;
        }
        // 获得求和位
        int s = a^b;
        // 获得进位,然后需要向做移动一位表示进位
        int c = ((a&b)<<1);
        return add(s,c);
    }

(二)减法

本质上是使用加法来做的,只是加了一个负数嘛

首先我们得先将被减数取反(a取反就是~a+1,补码知道吧)

   // 取得相反数
    int adverse(int a){
        return add(~a,1);
    }
    // 减法函数 其中add就是前面的加法函数
    int subtract(int a,int b){
        return add(a,adverse(b));
    }

(三)乘法

这个本质上也是靠加法来实现的,比如说6 * 100, 不就6个100相加吗?

不过我们要多考虑的事情是 正负的 处理

    // 负数返回-1,正数返回0
    int getsign(int i){
        return (i >> 31);
    }
    // 如果为负数,则进行取反
    int toPositive(int a) {
        if (a >> 31 == -1)
            // 进行取反
            return add(~a, 1);
        else
            return a;
    }

然后就用加法实现呗

int multiply(int a, int b){
        boolean flag = true;
        // 如果相乘为正数,flag为false
        if (getsign(a) == getsign(b))
            flag = false;
        // 将a取正数
        a = toPositive(a);
        b = toPositive(b);
        int re = 0;

        while (b!=0) {
            // 相加
            re = add(re, a);
            // b进行次数减一
            b = subtract(b, 1);
        }
        // 假如结果是负数,则进行取反
        if (flag)
            re = adverse(re);
        return re;
    }

但是 若是单纯靠加法来实现,数字比较大的时候感觉实在太慢了。

考虑我们现实生活中手动求乘积的过程,这种方式同样适用于二进制,下面我以13*14为例,演示如何用手动计算的方式求乘数和被乘数绝对值的乘积。

步骤和十进制的类似

(1) 判断乘数是否为0,为0跳转至步骤(4)
(2) 将乘数与1作与运算,确定末尾位为1还是为0,如果为1,则相加数为当前被乘数;如果为0,则相加数为0;将相加数加到最终结果中;
(3) 被乘数左移一位,乘数右移一位;回到步骤(1)
(4) 确定符号位,输出结果;

实现的话,就这样

int multiply(int a, int b) {
        boolean flag = true;
        // 如果相乘为正数,flag为false
        if (getsign(a) == getsign(b))
            flag = false;
        // 将a取正数
        a = toPositive(a);
        b = toPositive(b);
        int re = 0;
        while (b!=0) {
            // 假如b的最后一位为1  不就是奇偶的判断嘛 上面骚操作提过了 别问 问就是看上面
            if((b&1) == 1){
                // 相加
                re = add(re, a);
            }
            b = (b>>1);
            a = (a<<1);
        }
        // 假如结果是负数
        if (flag)
            re = adverse(re);
        return re;
    }

(四)除法

除法运算很容易想到可以转换成减法运算,就是不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。

这里需要注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为证,异号为负;余数的符号和被除数一样。

实现上与乘法第一种思路大同小异,但是效率低。

要想提高效率,可以参考乘法实现的第二种思路就是列竖式。这里就只提供第二种思路咯

 int divide(int a,int b){
        boolean flag = true;
        // 如果相除为正数,flag为false
        if (getsign(a) == getsign(b))
            flag = false;
        // 将a取正数
        a = toPositive(a);
        b = toPositive(b);
        int re = 0;
        int i = 31;
        while(i>=0){
            // 如果够减
            // 不用(b<<i)<a是为了防止溢出
            if((a>>i)>=b){
                // re代表结果
                re = add(re, 1<<i);
                a = subtract(a, (b<<i));
            }
            // i减一
            i = subtract(i, 1);
        }
        // 假如结果是负数
        if (flag)
            re = adverse(re);
        return re;
    }

done,我女朋友们看了都说好!


转载:https://blog.csdn.net/qq_42322103/article/details/104033181
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场