计算机系统依赖二进制计数法,可以说计算机的世界中只有0和1。我们所编写的程序最终到计算机都会转换成一个个0和1.
什么是二进制
我们平时生活中使用的都是十进制计数法,由0-9是个数字组成,逢十进一,高位在左,低位在右,从左往右依次书写。
二进制则是由0和1组成,逢二进一,高位在左,低位在右,从左往右依次书写。
例如数字 53
-
十进制
53 = 5 x 10^1 + 3 x 10^0 -
二进制
53 = 1 x 2^5 + 1 x 2^4 + 0 x 2^3 + 1 x 2^2 + 0 x 2^1 + 1 x 2^0
可以看出来 十进制就是以10为基数,二进制就是以2为基数。 同理以几为基数就是几进制。
程序中如何应用
import java.math.BigInteger;
public class Test {
//Description: 十进制转换成二进制
public static String decimalToBinary(int decimalSource) {
BigInteger bi = new BigInteger(String.valueOf(decimalSource)); //转换成BigInteger类型,默认是十进制
return bi.toString(2); //参数2指定的是转化成二进制
}
//Description: 二进制转换成十进制
public static int binaryToDecimal(String binarySource) {
BigInteger bi = new BigInteger(binarySource, 2); //转换为BigInteger类型,参数2指定的是二进制
return Integer.parseInt(bi.toString()); //默认转换成十进制
}
public static void main(String[] args) {
int a = 53;
String b = "110101";
System.out.println(String.format("数字%d的二进制是%s", a, decimalToBinary(a))); //获取十进制数53的二进制数
System.out.println(String.format("数字%s的十进制是%d", b, binaryToDecimal(b))); //获取二进制数110101的十进制数
}
}
计算机为什么使用二进制
这是跟组成计算机系统的逻辑电路有关,逻辑电路通常只有接通、断开两种状态,这就跟二进制很适合,用0表示断开状态,用1表示接通状态。因此每位数据只有断开和接通两种状态,所以即使系统受到干扰,它仍能可靠的分辨出数字是0还是1。二进制的数据表达具有抗干扰能力强、可靠性高的优点。相比之下用其他进制表示,系统的复杂度则会非常复杂还容易出错,例如十进制则需要设计10种状态的电路,想想就会很复杂。
关于符号位
符号位是有符号二进制数中的最高位,我们需要它来表示负数。在计算机操作系统中,计算机CPU只实现了加法器,而没有减法器。所以如果是一个减法,3-2 = 3 + (-2)。所以我们需要一个符号位来表示负数。所以人们将二进制数分为有符号数(signed)和无符号数(unsigned)。
如果有符号数,那么最高位就是符号位,0表示正数,1表示负数。
关于溢出
现实中的计算机系统,总有一个物理上的极限(比如说晶体管的大小和数量),因此不可能表示无穷大或者无穷小的数字。对计算机而言,无论是何种数据类型,都有一个上限和下限。
在Java中,int 型是32位,它的最大值也就是上限是2^31-1(最高位是符号位,所以是2的31次方而不是32次方),最小值也就是下限是 -2^31。而long型是 64 位,它的最大值,也就是上限是2^63 -1;最小值,也就是下限是-2^63。一旦某个数字超过了这些限定,就会发生溢出。如果超出上限,就叫上溢出(overflow)。如果超出了下限,就叫下溢出(underflow)。
n位数字的最大的正值,其符号位为 0,剩下的 n-1 位都为 1,再增大一个就变为了符号位为 1,剩下的 n-1 位都为 0。而符号位是 1,后面 n-1 位全是 0,我们已经说过这表示 -2^(n-1),也就是说上溢出之后又从下限开始,周而复始,这就相当于余数和取模的概念。
例如我们生活中的一周的概念。最后一天周日加上一天就是第一天周一。这个周期就是7。而对于二进制就是(2^(n-1) -1)-(-2^(n-1)) +1=2x2^(n-1) -1+1=2^n -1+1。所以2^n-1+1就是这个周期。
二进制的原码、反码及补码
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。
- 原码:就是二进制的原始表示形式。
- 反码:正数的反码是它本身,负数的反码是除符号位以外全部取反。
- 补码:正数补码是它本身,负数的补码为反码+1。
例如 3 -2 = 3 + (-2)
因为计算机没有减法器,所以计算机的减法是利用溢出的周期特性,也就是取模,来完成减法。
如果按照原码来进行
000.... 00011
+ 100.... 00010
———————————————
100.... 00101
结果为-5明显不对
如果按照计算机的溢出取模特性来看。 3 - 2 = (3-2)+(2^n -1 + 1)= 3 + (2^n -1 + 1 - 2)
2^n - 1 在不考虑符号位的情况下,为 n-1 位个1,2^n -1 -2 如下:
11111..... 111
-00000..... 010
——————————————————
11111..... 101
从结果可以看出
2^n -1 -2 的结果就是 2 除符号位以外全部取反,由于负数-2和正数2的原码,除了符号位之外都是相同的,所以,2^n-1-2 也相当于对负数 -2 的二进制原码,除了符号位之外按位取反。我们把 2^n-1-2 所对应的编码称为负数 -2 的反码。所以,-2 的反码就是 1111…1101
所以 3 - 2 = (3-2)+(2^n -1 + 1)= 3 + (2^n -1 + 1 - 2) = 3的原码 + (-2)的反码 + 1
由于负数反码+1 是补码。所以
1111....1101
+0000....0001
————————————————
1111....1110
3 - 2 = (3-2)+(2^n -1 + 1)= 3 + (2^n -1 + 1 - 2) = 3的原码 + (-2)的反码 + 1 = 3的原码 + (-2)的补码。
计算机是通过补码来运行二进制减法。所以 3 - 2 = 3的补码 + (-2)的补码。由于正数补码是它本身。所以最终结果如下
0000....00011
+1111....11110
---------------
0000....00001
最后结果为 1.
这就是计算机的减法利用溢出来正确计算。
二进制的位操作
位操作也叫位运算,直接针对内存中的二进制进行操作,非常快。位操作包括向左位移,向右位移,以及与、或、异或的逻辑操作。
向左位移
例如现在我们的二进制数是 110101 向左位移一位,则变成 1101010 。计算十进制数后发现值从53 变成 106,变成53的2倍,所以二进制左移一位,就是数字乘以2,数字翻倍。
向右位移
我们将 110101 向右位移一位 则变成 011010 ,计算十进制发现值从53 变为了 26 ,是53除以2的整数商部分。所以二进制右移一位,就是将数字除以2并取整数商部分的操作。
代码示例
import java.math.BigInteger;
public class Test1 {
// 向左移位
public static int leftShift(int num, int m) {
return num << m;
}
//向右移位
public static int rightShift(int num, int m) {
return num >>> m;
}
public static void main(String[] args) {
int num = 53;
int m = 1;
System.out.println(String.format("数字%d的二进制向左移%d位是%d", num, m, leftShift(num, m))); //测试向左移位
System.out.println(String.format("数字%d的二进制向右移%d位是%d", num, m, rightShift(num, m))); //测试向右移位
System.out.println();
m = 3;
System.out.println(String.format("数字%d的二进制向左移%d位是%d", num, m, leftShift(num, m))); //测试向左移位
System.out.println(String.format("数字%d的二进制向右移%d位是%d", num, m, rightShift(num, m))); //测试向右移位
}
}
上面的例子中向左位移表示是 << 而向右则是 >>> ,其实 右移分为两种情况,一种是逻辑右移 >>,左边补0,一种是算数位移 >>> ,除符号位不变,其余右移,并左边补1。
造成这种情况的原因在于最高位的一位为符号位,0代表正,1代表负。所以在右移的时候需要考虑这个高位符号位。而在左移的时候不需要考虑高位补1还是补0,只需要在低位补0,所以没有逻辑和算数之分。
逻辑右移
1110… 110101 >> 0111 … 110101
算数右移
1110… 110101 >>> 1111… 110101
这里的m代表位移的位数。1次就是*2 或者/2, 3则是 * 2^3 或者 / 2^3.
或运算
逻辑或的意思就是,参与操作的位中,只要有一个为1 那么最终结果就是1.
110101
或
100111
-------
110111
与运算
逻辑与的意思就是,参与操作的位中,必须全部为1,那么最终结果才是1,其余都为0。
110101
与
100111
-------
100101
异或操作
异或具有排异性,也就是说如果参与操作的位相同,那么最终结果就为 0(假),否则为 1(真)。所以,如果要得到 1,参与操作的两个位必须不同,这就是此处“异”的含义。
110101
异或
100111
-------
010010
代码示例
import java.math.BigInteger;
public class Test2 {
/**
* @Description: 二进制按位“或”的操作
* @param num1-第一个数字,num2-第二个数字
* @return 二进制按位“或”的结果
*/
public static int or(int num1, int num2) {
return (num1 | num2);
}
/**
* @Description: 二进制按位“与”的操作
* @param num1-第一个数字,num2-第二个数字
* @return 二进制按位“与”的结果
*/
public static int and(int num1, int num2) {
return (num1 & num2);
}
/**
* @Description: 二进制按位“异或”的操作
* @param num1-第一个数字,num2-第二个数字
* @return 二进制按位“异或”的结果
*/
public static int xor(int num1, int num2) {
return (num1 ^ num2);
}
public static void main(String[] args) {
int a = 53;
int b = 35;
System.out.println(String.format("数字%d(%s)和数字%d(%s)的按位‘或’结果是%d(%s)",
a, decimalToBinary(a), b, decimalToBinary(b), or(a, b), decimalToBinary(or(a, b)))); //获取十进制数53和35的按位“或”
System.out.println(String.format("数字%d(%s)和数字%d(%s)的按位‘与’结果是%d(%s)",
a, decimalToBinary(a), b, decimalToBinary(b), and(a, b), decimalToBinary(and(a, b)))); //获取十进制数53和35的按位“与”
System.out.println(String.format("数字%d(%s)和数字%d(%s)的按位‘异或’结果是%d(%s)",
a, decimalToBinary(a), a, decimalToBinary(a), xor(a, a), decimalToBinary(xor(a, a)))); //获取十进制数53和35的按位“异或”
}
}
位操作的应用
1.验证奇偶性
通过观察你可以发现偶数的二进制最后一位总是0,而奇数的最后一位总是1。因此可以通过将给定数字与数字1的二进制进行按位与操作。取得这个数的最后一位,然后进行判断。
n & 1 == 0 偶数
n & 1 == 1 奇数
2.交换两个数字
通常交换两个数字,我们需要一个中间临时变量来存放交换的值。而通过异或操作我们可以不用临时变量
例如 x,y交换
- x = ( x ^ y );
- y = x ^ y; 将 1的值代入2 得到 y = x ^ y ^ y ;由于 y ^ y = 0 ,所以 y = x ^ 0 = x;
- x = x ^ y; 将1和2的结果代入3 ,得到 x = x ^ y ^ x ; 由于 x ^ x = 0;所以 x = 0 ^ y = y;
完成交换。 这里利用了异或的两个特性,1.任何数与他本身异或等于0;2.任何数与0异或等于它本身。
3.集合操作
例如两个集合 {1,3,7} 和 {1,4,8}两个,我们将其转换为两个8位的二进制,第一个转化为01000101 ;第二个转换为 10001001;
我们将他们进行与操作结果是 00000001,只有第一位为1,所以两个集合的交集就是{1}.
进行或操作结果是11001101 ,第1,3,4,7,8位为1,所以两个集合的并集就是{1,3,4,7,8}。