追本溯源 二进制


计算机系统依赖二进制计数法,可以说计算机的世界中只有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交换

  1. x = ( x ^ y );
  2. y = x ^ y; 将 1的值代入2 得到 y = x ^ y ^ y ;由于 y ^ y = 0 ,所以 y = x ^ 0 = x;
  3. 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}。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值