二进制位运算是最贴近计算机真实运算操作,通过位运算,我们可以高效的完成各种基础运算(加减乘除取余等),我们还可以使用位运算巧妙的完成原本很复杂的工作,真正理解计算机,我们才能更好的使用计算机。
我将通过基础理解开始,讲解到 Java 中的一些实际应用。
本场Chat中,将学到一下内容:
- 对原码、反码、补码等基础进行重拾
- 与或异或移位等正负数运算细节
- 正负数位运算的操作
二进制位运算是最贴近计算机真实运算操作,通过位运算,我们可以高效的完成各种基础运算(加减乘除取余等),我们还可以使用位运算巧妙的完成原本很复杂的工作,真正理解计算机,我们才能更好的使用计算机。在这一片文章,我将通过基础理解开始,讲解到 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}
向左位移符号“>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,个位数 3 加上 9 为 2;十位数 1 加上 0 为 1; 最终结果为 12;
- 步骤二:只考虑进位,结果存为 carry,3 + 9 有进位,进位的值为 10;
- 步骤三:如果步骤二所得进位结果 carry 不为 0,对步骤一所得 sum 以及步骤二所得 carry,重复步骤一、二、三。如果 carry 为 0 则结束,最终结果为步骤一所得 sum。
这里其实就是对 sum = 12 和 carry = 10 重复以上三个步骤
- (a) 不考虑进位,分别对各位数进行相加,sum = 22;
- (b) 只考虑进位: 上一步没有进位,所以 carry = 0; (c) 步骤 2carry = 0,结束,结果为 sum = 22.
这是我们在十进制中进行的运算演示,那我们换成二进制看看是不是一样,13 的二进制为 0000 1101,9 的二进制为 0000 1001:
- 第一步:不考虑进位,分别对各位数进行相加,sum = 0000 1101 + 0000 1001 = 0000 0100
- 第二步:考虑进位, 有两处进位,第 0 位和第 3 位,只考虑进位的结果为: carry = 0001 0010
- 第三步:carry == 0 ?,不为 0,重复步骤一 、二 、三;为 0 则结束,结果为 sum。
本例中
- 不考虑进位 sum = 0001 0110;
- 只考虑进位 carry = 0;
- carry == 0,结束,结果为 sum = 0001 0110
转换成十进制刚好是 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; i++; } } return res; }
除法
除法运算很容易想到可以转换成减法运算,即不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。这里需要注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为证,异号为负;余数的符号和被除数一样。 计算机是一个二元的世界,所有的 int 型数据都可以用[2^0, 2^1,…,2^31]这样一组基来表示(int 型最高 31 位)。不难想到用除数的 2^31,2^30,…,2^2,2^1,2^0 倍尝试去减被除数,如果减得动,则把相应的倍数加到商中;如果减不动,则依次尝试更小的倍数。这样就可以快速逼近最终的结果。
2 的 i 次方其实就相当于左移 i 位,为什么从 31 位开始呢?因为 int 型数据最大值就是 2^31 啊。
int division(int a,int b){ int res; if(a
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?