计算机为什么用二进制?
因为简单!
计算机的底层是基于晶体管实现的,可以简单地理解为一组开关,打开就表示 1,关闭就表示 0,所以,采用二进制的话,计算机只需要简单地打开/关闭这些开关即可。
事物只有两种状态:有和无!用数字表示就是 0 和 1!
假设我们的计算机有 1 个开关,打开表示天亮,关闭表示天黑;打开表示对,关闭表示错;打开表示 1,关闭表示 0,这种例子有很多。那么如果是十进制呢?用谁表示打开,用谁表示关闭呢?又怎么表示 1,怎么表示 9 呢?太麻烦了!
我们来看二进制的最大优点:位运算。
我们知道:p 与 q 等于取交集,全部为 1 才为 1;p 或 q 等于取并集,全部为 0 才为 0。
OK,现在我们是二进制,只有 0 和 1。那么根据上述法则:
100∩011=000100∩011=000,100∪011=111100∪011=111。
二进制优化性能案例:
有个问题:给定一个数 n,怎么判断 n 是不是 2 的 n 次方呢?很简单啊,直接求余就行了。
void is2Power(int n) {
if (n <= 0) return false;
int temp = n;
while (temp > 1) {
if (temp % 2 != 0) return false;
temp /= 2;
}
return true;
}
确实很好,但是还能更好。看下面代码:
public boolean isPowerOfTwo(int n) {
return (n>0) && ((n & (n - 1)) == 0);
}
我们知道,10 的 n 次方最高位是 1,其他位都是 0;2 的 n 次方也是同理。也就是说:如果一个数 a 是 2 的 n 次方,那么 a 的最高位就是 1,其他位全是 0;那么,a-1 呢,就变成高位是 0,其他位全是 1 了。
比如:a = 10000,a-1 就是 01111,根据真值表,我们知道:
10000∩01111=010000∩01111=0
所以如果 a 是 2 的 n 次方,那么就有:
𝑎∩(𝑎−1)=0a∩(a−1)=0
根据这个特性,我们只需要进行简单的按位与运算,就能很快得到正确的结果。这就是位运算的强大之处。大家可以在 leetcode 上试一下,对比下运行速度便知。
灵活运用二进制的位运算不仅能提高速度,熟练使用二进制还能节省内存。
在很多人的认知中,最省内存的就是基本数据类型,也就是布尔值、短整型、byte 类型等。
但是,这不是最省的。比如,我要存储一个人的年龄和性别,我们可以用两个字段:
int age;
boolean isMan;
这大概是所有人的写法了,也确实符合逻辑,但是,我们可以用更少的空间来记录的。我们知道,一个人的年龄最多也就是三位数,一个人的性别要么是男、要么是女,雌雄同体和人妖除外。
那么,一位数就能表示性别,三位数就能表示年龄。也就是说,一个二进制就能表示性别:0 表示女,1 表示男。人的年龄很少超过 120 岁,而一个 byte 最大表示 127,我们用一个 byte 就能表示年龄,一个 byte 是 8 位二进制,那就是说,我们只需要 9 个二进制位就能表示人的年龄和性别。也就是两个 byte。而一个 int 是 4 个 byte 呢,这样我们就直接节省了一半以上的内存。
那么二进制怎么表示一个 32 岁的男性呢?像这样:1_0010_0000,我们用首位的 1 表示性别,后面的 8 位表示年龄。这样只需要两个 byte 就足够。
有人可能会说:这有点小题大做啊,至于吗?
嗯,对于大部分场景,是意义不大的,但是对于一些很多人用的公共库、工程代码以及一些追求极致的设备上,这就很重要了。
比如 Java 的字节码,我们知道,我们写的语言都是最后要跑在 CPU 里面的,而 CPU 只认识机器码,也就是二进制;所以我们写的代码要有个编译过程,Java 语言的这个过程的产物就是字节码文件,这个文件要尽可能紧凑,尽可能小,那么上述的小题就不大做了。
我们可能会看到一些源码里面有很多 flag 变量,对这些 flag 进行按位与或按位或运算来检测标记,从而判断是否开启了某个功能。他为什么不直接用布尔值呢?很简单,这样效率高还节省内存。比如Android源码中的测量过程,就用到MeasureSpec这个类,它是一个int类型,用高两位表示测量模式,用低 30 位表示测量值,这样只用一个变量就保存了多个信息,有兴趣的可以去看下相关源码。