You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 01 | 二进制:不了解计算机的源头,你学什么编程
我们都知道,计算机的起源是数学中的二进制计数法。可以说,没有二进制,就没有如今的计算机系统。那什么是二进制呢?为什么计算机要使用二进制,而不是我们日常生活中的十进制呢?如何在代码中操作二进制呢?专栏开始,我们就从计算机认知的起源——二进制出发,讲讲它在计算机中的“玄机”。
## 什么是二进制计数法?
为了让你更好地理解二进制计数法,我们先来简单地回顾一下人类计数的发展史。
原始时代,人类用路边的小石子,来统计放牧归来的羊只数量,这表明我们很早就产生了计数的意识。后来,罗马人用手指作为计数的工具,并在羊皮上画出Ⅰ、Ⅱ、Ⅲ来代替手指的数量。表示一只手时,就写成“Ⅴ”形,表示两只手时,就画成“ⅤⅤ”形等等。
公元3世纪左右印度数学家也有说法是阿拉伯人发明了阿拉伯数字。阿拉伯数字由从0到9这样10个计数符号组成并采取**进位制法**,高位在左,低位在右,从左往右书写。由于阿拉伯数字本身笔画简单,演算便利,因此它们逐渐在各国流行起来,成为世界通用的数字。
日常生活中,我们广泛使用的十进制计数法,也是基于阿拉伯数字的。这也是十进制计数法的基础。因此,相对其他计数方法,十进制最容易被我们所理解。
让我们来观察一个数字2871。
![](https://static001.geekbang.org/resource/image/d9/3d/d99f094c432638924f8665a178162c3d.jpg)
其中^表示幂或次方运算。十进制的数位千位、百位、十位等全部都是10^n的形式。需要特别注意的是任何非0数字的0次方均为1。在这个新的表示式里10被称为十进制计数法的**基数**,也是十进制中“十”的由来。这个我想你应该好理解,因为这和我们日常生活的习惯是统一的。
明白了十进制我们再试着用类似的思路来理解二进制的定义。我以二进制数字110101为例解释给你听。我们先来看这里110101究竟代表了十进制中的数字几呢
刚才我们说了十进制计数是使用10作为基数那么二进制就是使用2作为基数类比过来**二进制的数位就是2^n的形式**。如果需要将这个数字转化为人们易于理解的十进制,我们就可以这样来计算:
![](https://static001.geekbang.org/resource/image/c6/c0/c6ae1772d7bf369aa9939fc00ca7b5c0.jpg)
按照这个思路我们还可以推导出八进制以8为基数、十六进制以16为基数等等计数法很简单我在这里就不赘述了。
至此,你应该已经理解了什么是二进制。但是仅有数学的理论知识是不够的,结合相关的代码实践,相信你会有更深刻的印象。
基于此我们来看看二进制和十进制数在Java语言中是如何互相转换的并验证一下我们之前的推算。我这里使用的是Java语言来实现的其他主流的编程语言实现方式都是类似的。
这段代码的实现采用了Java的BigInteger类及其API函数我都加了代码注释并且穿插一些解释你应该可以看懂。
首先我们引入BigInteger包通过它和Integer类的API函数进行二进制和十进制的互相转换。
```
import java.math.BigInteger;
public class Lesson1_1 {
/**
* @Description: 十进制转换成二进制
* @param decimalSource
* @return String
*/
public static String decimalToBinary(int decimalSource) {
BigInteger bi = new BigInteger(String.valueOf(decimalSource)); //转换成BigInteger类型默认是十进制
return bi.toString(2); //参数2指定的是转化成二进制
}
/**
* @Description: 二进制转换成十进制
* @param binarySource
* @return int
*/
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, Lesson1_1.decimalToBinary(a))); //获取十进制数53的二进制数
System.out.println(String.format("数字%s的十进制是%d", b, Lesson1_1.binaryToDecimal(b))); //获取二进制数110101的十进制数
}
```
这段代码运行的结果是十进制数字53的二进制是110101二进制数字110101的十进制是53。
好了,关于十进制和二进制的概念以及进制之间的相互转换,你应该都很清楚了。既然有十进制,又有二进制,你可能就要问了,为啥计算机使用的是二进制而不是十进制呢?
## 计算机为什么使用二进制?
我觉得,计算机使用二进制和现代计算机系统的硬件实现有关。组成计算机系统的逻辑电路通常只有两个状态,即开关的接通与断开。
断开的状态我们用“0”来表示接通的状态用“1”来表示。由于每位数据只有断开与接通两种状态所以即便系统受到一定程度的干扰时它仍然能够可靠地分辨出数字是“0”还是“1”。因此在具体的系统实现中二进制的数据表达具有抗干扰能力强、可靠性高的优点。
相比之下如果用十进制设计具有10种状态的电路情况就会非常复杂判断状态的时候出错的几率就会大大提高。
另外二进制也非常适合逻辑运算。逻辑运算中的“真”和“假”正好与二进制的“0”和“1”两个数字相对应。逻辑运算中的加法“或”运算、乘法“与”运算以及否定“非”运算都可以通过“0”和“1”的加法、乘法和减法来实现。
## 二进制的位操作
了解了现代计算机是基于二进制的,我们就来看看,计算机语言中针对二进制的位操作。这里的**位操作**,也叫作**位运算**,就是直接对内存中的二进制位进行操作。常见的二进制位操作包括向左移位和向右移位的移位操作,以及“或”“与”“异或”的逻辑操作。下面我们一一来看。
### 向左移位
我们先来看向左移位。
二进制110101向左移一位就是在末尾添加一位0因此110101就变成了1101010。请注意这里讨论的是数字没有溢出的情况。
所谓**数字溢出**就是二进制数的位数超过了系统所指定的位数。目前主流的系统都支持至少32位的整型数字而1101010远未超过32位所以不会溢出。如果进行左移操作的二进制已经超出了32位左移后数字就会溢出需要将溢出的位数去除。
![](https://static001.geekbang.org/resource/image/cd/76/cdbeb658035f275aa941a0d3f6eac876.jpg)
在这个例子中如果将1101010换算为十进制就是106你有没有发现106正好是53的2倍。所以我们可以得出一个结论**二进制左移一位,其实就是将数字翻倍**。
### 向右移位
接下来我们来看向右移位。
二进制110101向右移一位就是去除末尾的那一位因此110101就变成了11010最前面的0可以省略。我们将11010换算为十进制就是26正好是53除以2的整数商。所以**二进制右移一位就是将数字除以2并求整数商的操作**。
![](https://static001.geekbang.org/resource/image/8d/34/8df282639b609d5269582c789796c334.jpg)
下面我们来看看,用代码如何进行移位操作。
```
import java.math.BigInteger;
public class Lesson1_2 {
/**
* @Description: 向左移位
* @param num-等待移位的十进制数, m-向左移的位数
* @return int-移位后的十进制数
*/
public static int leftShift(int num, int m) {
return num << m;
}
/**
* @Description: 向右移位
* @param num-等待移位的十进制数, m-向右移的位数
* @return int-移位后的十进制数
*/
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, Lesson1_2.leftShift(num, m))); //测试向左移位
System.out.println(String.format("数字%d的二进制向右移%d位是%d", num, m, Lesson1_2.rightShift(num, m))); //测试向右移位
System.out.println();
m = 3;
System.out.println(String.format("数字%d的二进制向左移%d位是%d", num, m, Lesson1_2.leftShift(num, m))); //测试向左移位
System.out.println(String.format("数字%d的二进制向右移%d位是%d", num, m, Lesson1_2.rightShift(num, m))); //测试向右移位
}
```
这段代码的运行结果是数字53向左移1位是106数字53向右移1位是26。数字53向左移3位是424数字53向右移3位是6。
我来解释一下。其中移位1次相当于乘以或除以2而移位3次就相当于乘以或除以8即2的3次方。细心的话你可能已经发现Java中的左移位和右移位的表示是不太一样的。
**左移位是<<,那右移位为什么是>>>而不是>>呢?**实际上,>>也是右移操作。简单来说之所以有这两种表达方式根本原因是Java的二进制数值中最高一位是符号位。这里我给你详细解释一下。
当符号位为0时表示该数值为正数当符号位为1时表示该数值为负数。我们以32位Java为例数字53的二进制为110101从右往左数的第32位是0表示该数是正数只是通常我们都将其省略。
![](https://static001.geekbang.org/resource/image/83/ef/831a21734048b1fc3e357609527175ef.jpg)
如果数字是-53呢那么第32位就不是0而是1。请注意我这里列出的是补码。
![](https://static001.geekbang.org/resource/image/85/1e/857248d65d7c4b746b3677d45b2a2c1e.jpg)
那么这个时候向右移位就会产生一个问题对于符号位特别是符号位为1的时候我们是否也需要将其右移呢因此Java里定义了两种右移**逻辑右移**和**算术右移**。逻辑右移1位左边补0即可。
![](https://static001.geekbang.org/resource/image/74/c7/745a4880417a4dcb62f88bad7be800c7.jpg)
算术右移时保持符号位不变除符号位之外的右移一位并补符号位1。补的1仍然在符号位之后。
![](https://static001.geekbang.org/resource/image/b0/07/b00b38ba0e8e2349a64b52905852e107.jpg)
逻辑右移在Java和Python语言中使用>>>表示,而算术右移使用>>表示。如果你有兴趣,可以自己编码尝试一下,看看这两种操作符输出的结果有何不同。
在C或C++语言中,逻辑右移和算数右移共享同一个运算符>>。那么编译器是如何决定使用逻辑右移还是算数右移呢答案是取决于运算数的类型。如果运算数类型是unsigned则采用逻辑右移而是signed则采用算数右移。如果你针对unsigned类型的数据使用算数右移或者针对signed类型的数据使用逻辑右移那么你首先需要进行类型的转换。
由于左移位无需考虑高位补1还是补0符号位可能为1或0所以不需要区分逻辑左移和算术左移。
### 位的“或”
我们刚才说了二进制的“1”和“0”分别对应逻辑中的“真”和“假”因此可以针对位进行逻辑操作。
逻辑“或”的意思是参与操作的位中只要有一个位是1那么最终结果就是1也就是“真”。如果我们将二进制110101和100011的每一位对齐进行按位的“或”操作就会得到110111。
![](https://static001.geekbang.org/resource/image/83/15/8394b6daf1d9727069736506332c4915.jpg)
### 位的“与”
同理我们也可以针对位进行逻辑“与”的操作。“与”的意思是参与操作的位中必须全都是1那么最终结果才是1否则就为0。如果我们将二进制110101和100011的每一位对齐进行按位的“与”操作就会得到100001。
![](https://static001.geekbang.org/resource/image/b1/19/b1c520385f4e5a5b719c393f9e1d0019.jpg)
### 位的“异或”
逻辑“异或”和“或”有所不同它具有排异性也就是说如果参与操作的位相同那么最终结果就为0否则为 1。所以如果要得到1参与操作的两个位必须不同这就是此处“异”的含义。我们将二进制110101和100011的每一位对齐进行按位的“异或”操作可以得到结果是10110。
![](https://static001.geekbang.org/resource/image/c7/87/c7ae84301b2742b6714c01a77e6a6b87.jpg)
我总结一下“异或”操作的本质其实就是所有数值和自身进行按位的“异或”操作之后都为0。而且要通过“异或”操作得到0也必须通过两个相同的数值进行按位“异或”。这表明了两个数值按位“异或”结果为0是这两个数值相等的必要充分条件可以作为判断两个变量是否相等的条件。
接下来我们来学习一下在代码中如何实现二进制的逻辑操作。Java中使用|表示按位的“或”,&表示按位“与”,^表示按位“异或”。
```
import java.math.BigInteger;
public class Lesson1_3 {
/**
* @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), Lesson2_3.or(a, b), decimalToBinary(Lesson1_3.or(a, b)))); //获取十进制数53和35的按位“或”
System.out.println(String.format("数字%d(%s)和数字%d(%s)的按位‘与’结果是%d(%s)",
a, decimalToBinary(a), b, decimalToBinary(b), Lesson2_3.and(a, b), decimalToBinary(Lesson1_3.and(a, b)))); //获取十进制数53和35的按位“与”
System.out.println(String.format("数字%d(%s)和数字%d(%s)的按位‘异或’结果是%d(%s)",
a, decimalToBinary(a), a, decimalToBinary(a), Lesson2_3.xor(a, a), decimalToBinary(Lesson1_3.xor(a, a)))); //获取十进制数53和35的按位“异或”
}
```
这段代码的运行结果是数字53(110101)和数字35(100011)的按位结果是55(110111)数字53(110101)和数字35(100011)的按位结果是33(100001)数字53(110101)和数字53(110101)的按位异或结果是0(0)。
## 小结
今天我们聊了二进制你可能会问学习二进制究竟有什么用呢平时的编程中我们好像并没有使用相关的知识啊确实目前的高级语言可以帮助我们将人类的思维逻辑转换为使用0和1的机器语言我们不用再为此操心了。但是二进制作为现代计算机体系的基石这些基础的概念和操作你一定要非常了解。
二进制贯穿在很多常用的概念和思想中例如逻辑判断、二分法、二叉树等等。逻辑判断中的真假值就是用二进制的1和0来表示的二分法和二叉树都是把要处理的问题一分为二正好也可以通过二进制的1和0来表示。因此理解了二进制你就能更加容易地理解很多计算机的数据结构和算法也为我们后面的学习打下基础。
![](https://static001.geekbang.org/resource/image/20/74/209f1b06efdf9a7413fb793571c7ed74.jpg)
## 思考题
如果不使用Java语言自带的BigInteger类我们还有什么方法来实现十进制到二进制的转换呢提示可以使用二进制的移位和按位逻辑操作来实现。
欢迎在留言区交作业,并写下你今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。