面试官:你真的搞清位运算了么?
来源:     阅读:525
云上智慧
发布于 2020-04-24 18:54
查看主页

写在前面

二进制位运算是最贴近计算机真实运算操作,通过位运算,我们可以高效的完成各种基础运算(加减乘除取余等),我们还可以使用位运算巧妙的完成本来很复杂的工作,真正了解计算机,我们才能更好的使用计算机。在这一片文章,我将通过基础了解开始,讲解到 Java 中的少量实际应用。

机器数和机器数的真值

一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用机器数的最高位存放符号,正数为 0,负数为 1。举个例子,比方在机器字长为 8 位的情况下(机器字长是指计算机直接解决的二进制数据的位数,它决定了计算机的运算精度,一般是 8 的整数倍,8 位、16 位、32 位、64 位、128 位),十进制中的+3,转换成二进制就是 0000 0011,假如是-3,转换成二进制就是 1000 0011。转换的二进制数 0000 0011 和 1000 0011 就是机器数。

这里我们还需要知道的就是机器数的真值,因为机器数的第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 1000 0011,其最高位 1 代表负,其真正数值是-3,而不是形式值 131(1000 0011 转换成十进制等于 131),所以,为了区别起见,将带符号的机器数对应的真正数值成为机器数的真值。比方 0000 0001 的真值 = +000 0001 = +1,1000 0001 的真值 = –000 0001 = –1

原码、反码和补码的基础概念和计算方法

上面我们理解了机器数,也就是二进制数,不过计算机要使用肯定的编码方法进行储存,原码、反码和补码就是机器存储一个具体数字的编码方式。

原码

原码就是符号位加上真值的绝对值,即可使用第一位表示符号,其他位表示值,比方:假如 8 位二进制:

[+1]原= 0000 0001

[-1]原= 1000 0001

第一位是符号位,由于第一位是符号位,所以 8 位二进制数的取值范围就是:(即第一位不表示值,只表示正负。)[1111 1111 , 0111 1111],也就是十进制的[-127 , 127](小声哔哔,其实可以说成原码是带符号的机器数)。

反码

正数的反码就是其本身,负数的反码是其原码的基础上,符号位不变,其他各个位取反。

[+1] = [0000 0001]原= [0000 0001]反

[-1] = [1000 0001]原= [1111 1110]反

补码

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

[+1] = [0000 0001]原= [0000 0001]反= [0000 0001]补

[-1] = [1000 0001]原= [1111 1110]反= [1111 1111]补

知道了这三个基本概念之后,值得一提的是,假如用反码相加,会产生两个零的问题(-0 和+0),所以我们使用补码,不仅仅修复了 0 的符号以及存在两个编码的问题,而且还能够多表示一个最低数。这就是为什么 8 位二进制,使用原码或者反码表示的范围为[-127, +127],而使用补码表示的范围为[-128, 127]。由于机器使用补码,所以对于编程中常用到的有符号的 32 位 int 类型,可以表示范围是: [-231, 231-1] 由于第一位表示的是符号位,而使用补码表示时又可以多保存一个最小值。

Java 中的运算符

注意了,以下所有的位运算都是通过补码进行的,正数的补码就是它本身,负数自己对应算,两个操作数都为正数,则结果直接取二进制转十进制,假如两个操作数其中有一个是负数或者者两个都为负数,则结果假如符号位是 1(即负的),则得到的是补码,需要从补码转到原码,再转换成十进制,假如结果符号位是 0,直接取二进制转十进制。也就是运算时逢负取补,结果是逢负取原。可以自己先把下面十进制数一律转换成二进制补码,而后带进去算,看一下是不是正确结果。

异或者运算符号是“^”,相同的为 0,不同的为 1,代码举例如下:

public static void main(String[] args) {    System.out.println("2^3 运算的结果是 :"+(2^3));    //打印的结果是:   2^3 运算的结果是 :1}//2 的二进制 0010,3 的二进制 0011,2^3 就为 0001,结果就是 1public static void main(String[] args) {    System.out.println("-2^3 运算的结果是 :"+(-2^3));    //打印的结果是:   -2^3 运算的结果是 :-3}//-2 的二进制补码 1110,3 的二进制 0011,-2^3 就为 0001,结果就是-3

与运算符号是“&”,只需有一个为 0 就为 0,代码举例如下:

public static void main(String[] args) {     System.out.println("2&3 运算的结果是 :"+(2&3));     //打印的结果是:   2&3 运算的结果是 :2}public static void main(String[] args) {     System.out.println("-2&3 运算的结果是 :"+(-2&3));     //打印的结果是:   -2&3 运算的结果是 :2}

或者运算符是“|”,只需有一个为 1 就是 1,代码举例如下:

public static void main(String[] args){    System.out.println("2|3 运算的结果是 :"+(2|3));    //打印的结果是: 2|3 运算的结果是 : 3}public static void main(String[] args){    System.out.println("-2|3 运算的结果是 :"+(-2|3));    //打印的结果是: -2|3 运算的结果是 : -1}

非运算符是“~”,就是各位取反,代码举例如下:

public static void main(String[] args){    System.out.println("~5 运算的结果是 :"+(~5));    //打印的结果是: ~5 运算的结果是 : -6}public static void main(String[] args){    System.out.println("~(-5)运算的结果是 :"+(~(-5)));    //打印的结果是: ~(-5)运算的结果是 : 4}

向左位移符号“<<,二进制向左移动 n 位,后面用 0 补齐

public static void main(String[] args) {     System.out.println("2<<3 运算的结果是 :"+(2<<3));     //打印的结果是:   2<<3 运算的意思是,向左移动 3 位,其结果是 :16}public static void main(String[] args) {     System.out.println("-2<<3 运算的结果是 :"+(-2<<3));     //打印的结果是:   -2<<3 运算的意思是,向左移动 3 位,其结果是 :-16}

向右位移符号“>>”,二进制向右移动 n 位,若值为正,则在高位插入 0,若值为负,则在高位插入 1

public static void main(String[] args) {     System.out.println("2>>3 运算的结果是 :"+(2>>3));     //打印的结果是:   2>>3 运算的意思是,向右移动 3 位,其结果是 :0}public static void main(String[] args) {     System.out.println("-2>>3 运算的结果是 :"+(-2>>3));     //打印的结果是:   -2>>3 运算的意思是,向右移动 3 位,其结果是 :-1}

无符号右移符号“>>>”,忽略符号位,空位都以 0 补齐。 “>>>”与“>>”唯一的不同是它无论原来的最左边是什么数,统统都用 0 填充。比方,byte 是 8 位的,-1 表示为 byte 型是 11111111(补码表示法) b>>>4 就是无符号右移 4 位,即 00001111,这样结果就是 15。(小声哔哔,别问我有没有无符号左移,等你真正融会贯通之后,你会发现这是一个很 low 的问题。)

public static void main(String[] args) {     System.out.println("16>>2 运算的结果是 :"+((16)>>2));     //打印的结果是:   16>>2 运算的结果是 :4}public static void main(String[] args) {     System.out.println("-16>>2 运算的结果是 :"+((-16)>>2));     //打印的结果是:   -16>>2 运算的结果是 :-4}public static void main(String[] args) {     System.out.println("16>>>2 运算的结果是 :"+((16)>>>2));     //打印的结果是:   16>>>2 运算的结果是 :4}public static void main(String[] args) {     System.out.println("-16>>>2 运算的结果是 :"+((-16)>>>2));     //打印的结果是:   -16>>>2 运算的结果是 :1073741820}

不用乘除算乘除

加法

以 13+9 为例,我们像这样来拆分这个运算过程:

这里其实就是对 sum = 12 和 carry = 10 重复以上三个步骤

这是我们在十进制中进行的运算演示,那我们换成二进制看看是不是一样,13 的二进制为 0000 1101,9 的二进制为 0000 1001:

本例中

转换成十进制恰好是 22。其实就是三个步骤,用形象的伪代码来了解如下,这次用 3+9 举例。

a = 0011, b = 1001;    start;    first loop;    1.1 sum = 1010    1.2 carry = 0010    1.3 carry != 0 , go on;    second loop;    2.1 sum = 1000;    2.2 carry = 0100;    2.3 carry != 0, go on;    third loop;    3.1 sum = 1100;    3.2 carry = 0000;    3.3 carry == 0, stop; result = sum;end
//1、递归形式实现int add(int a ,int b){    if (b == 0)        return a;    else{        int carry = (a & b) << 1;        a = a ^b;        return add(a,carry);    }}//非递归形式实现int add2(int a ,int b){   int carry;   while (b != 0){   carry = (a & b) << 1;       a = a ^b;       b = carry;   }   return a;}

减法

我们知道了位运算实现加法运算,那减法运算就相对简单少量了。我们实现了加法运算,自然的,我们会想到把减法运算 11 - 6 变形为加法运算 11 + (-6),即一个正数加上一个负数。是的,很聪明,其实我们的计算机也是这样操作的,那有的人会说为什么计算机不也像加法器一样实现一个减法器呢?对的,这样想当然是正当的,但是考虑到减法比加法来的复杂,实现起来比较困难。为什么呢?我们知道加法运算其实只有两个操作,加、 进位,而减法呢,减法会有借位操作,假如当前位不够减那就从高位借位来做减法,这里就会问题了,借位怎样表示呢?加法运算中,进位通过与运算并左移一位实现,而借位就真的不好表示了。所以我们自然的想到将减法运算转变成加法运算。怎样实现呢?

刚刚我们说了减法运算可转变成一个正数加上一个负数,那首先就要来看看负数在计算机中是怎样表示的。

+8 在计算机中表示为二进制的 1000,那-8 怎样表示呢?很容易想到,可以将一个二进制位(bit)专门规定为符号位,它等于 0 时就表示正数,等于 1 时就表示负数。比方,在 8 位机中,规定每个字节的最高位为符号位。那么,+8 就是 00001000,而-8 则是 10001000。这只是直观的表示方法,其实计算机是通过 2 的补码来表示负数的,那什么是 2 的补码(同补码,英文是 2’s complement,其实应该翻译为 2 的补码)呢?它是一种用二进制表示有号数的方法,也是一种将数字的正负号变号的方式,求取步骤:

简单来说就是取反加一!

int subtraction(int a ,int b){     b = ~b+1;     return this.add(a,b); }

乘法

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

图解

从上图的计算过程可以看出,假如乘数当前位为 1,则取被乘数左移一位的结果加到最终结果中;假如当前位为 0,则取 0 加到乘积中(加 0 也就是什么也不做)

      //a 被乘数,b 乘数      int multiplication(int a,int b){          int i = 0;          int res = 0;          //乘数不为 0          while (b != 0){              //解决当前位              //当前位是 1             if ((b & 1) == 1){                 res += (a << i);                 b = b >> 1;                 //记录当前是第几位                 i++;             }else {                 //当前位是 0                 b = b >> 1;                 i++;             }         }         return res;     }

除法

除法运算很容易想到可以转换成减法运算,即不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。这里需要注意的是符号确实定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为证,异号为负;余数的符号和被除数一样。 计算机是一个二元的世界,所有的 int 型数据都可以用[2^0, 21,…,231]这样一组基来表示(int 型最高 31 位)。不难想到用除数的 231,230,…,22,21,2^0 倍尝试去减被除数,假如减得动,则把相应的倍数加到商中;假如减不动,则依次尝试更小的倍数。这样即可以快速逼近最终的结果。

2 的 i 次方其实就相当于左移 i 位,为什么从 31 位开始呢?由于 int 型数据最大值就是 2^31 啊。

    int division(int a,int b){          int res;          if(a<b){              return 0;          }else{              res=division(subtraction(a, b), b)+1;          }          return res;     }
免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 服务器应用
相关推荐
女程序员年收入50万,想买30万的车,老公却说太贵,网友评论炸锅
你所不知道的 CSS 负值技巧与细节
手把手教你怎样套模板制作网页
前台面试每日 3+1 —— 第329天
快手蒙眼狂奔:一图看懂快手上市招股书:日活3亿,电商GMV达1096亿元!
首页
搜索
订单
购物车
我的