有趣的二进制

优秀应用算法都大量用到位运算,而位运算在工做中不多用到,因此借助其算法,咱们看一下位运算的优点以及应用,可是大多数教材只会教你们二进制和十进制如何互换,都是死记硬背式的,并无去讲解真正含义,换一个进制以后,依然不会,咱们回到最根本的一些计数方法上,从10进制来推算,但愿用一种更简单的方式介绍其原理。
算法


1、应用引入

咱们来看下protobuf 中的Varint算法,这个算法的目的是为了令一个整型占用更少的字节,好比小于127的数字,只需占用一个字节便可,小于16384的数字,采用2个字节便可。算法以下:数组

while (true) {
  if ((value & ~0x7F) == 0) {
    buffer[position++] = (byte) value;
    return;
  } else {
    buffer[position++] = (byte) ((value & 0x7F) | 0x80);
    value >>>= 7;
  }
}复制代码

咱们来看看具体图例:bash

咱们看到在小于2097153期间,占用空间会小于4个字节,这个优点还比较明显,不过也有弊端,好比超过268435456以后会有占用5个字节,考虑到大多数状况下,并不会应用到这么大的数字,优化空间方面仍是不错的。学习

经过上述算法实现,我发现优秀的应用算法都会大量用到了位运算,而位运算在工做中却不多用到。位运算速度要快于整数运算的,特别是整数乘法,须要10个或者更多个时钟,若果采用移位操做一个或者2个时钟就够了,不过因为咱们常采用十进制来进行算术运算,对二进制的位运算不够熟悉,阅读起来会比较耗费精力,因此借助上述算法实现,咱们分析一下位运算的优点以及应用,从而更好的理解二进制。上述代码中,有运用到移位操做,位运算,字节序等相关知识点,咱们一一分析。优化


2、进位制

咱们知道,计算机的存储和处理的信息都是以二进制的,虽然在编写程序的期间数运算仍是采用10进制表示,但到机器执行的时候,还会以2进制来进行处理。对于有10个指头的人来讲,熟知10进制是很天然的事情,你看教小孩子数学的时候,都是先从数指头开始的,那么如果咱们只有2个指头,是否是咱们如今会更好理解二进制了?ui


一、其余进制转换10进制

大多数教材会教你们二进制和十进制如何互换,但多数说都是死记硬背式的,并无去讲解真正含义,换一个进制以后,依然不会,咱们回到最根本的一些计数方法上,从10进制来推算。好比咱们看一个数字1001,采用十进制表示是:1x10^3+0*10^2+0*10^1+1*10^0。首先从右往左,咱们能够当作是从低位到高位,每高一位,指数+1,其次10进制是以10为底数,其三这个公式是采用10进制算术进行计算的(用什么进制算出答案就至关于把当前进制转换为了什么进制了)。这个方式适合全部的进制转换,理解了这个,后续的进制转换都会很容易理解。spa


2进制的比较简单,咱们直接忽略,咱们来看下应用到3进制,一样是1001,转换10进制公式:1x3^3+0*3^2+0*3^1+1*3^0=28,咱们发现只是底数改变,由于是3进制,因此以3为底数,另外计算方式仍是采用10进制算式计算,这代表用10进制算出的答案,就至关于3进制转换为10进制,1001转换为10进制就是28。code

那为什么不采用其余进制来计算?cdn

采用其余进制计算,那么其余进制的乘法口诀你的熟练一遍了,好比10进制的99乘法口诀,你用其余进制的乘法口诀得本身来演绎一遍了,如此这个和咱们的经常使用习惯有些相驳,换算起来会比较慢,因此通常采用十进制与其它进制互转或者做为中间步骤来处理。blog


二、10进制转换为其余进制

采用上述方法后,咱们已经能够作到全部进制转换,包括10进制转3进制,好比十进制28转换为3进制28=2*3+22,这个采用3进制(3的三进制表示10)来进行计算,可是会很麻烦。因此10进制转换其余进制,咱们常采用短除法,以下:

当前数不断除以3并把余数做为新的最高位,28除以3余1,1为“个位”,9除以3余0,0为“十位”,3除以3余0,0为“百位”,最终的1是“千位”。

若是咱们有注意到前面的3进制转10进制算法,咱们能够发现短除法实际上是3进制转10进制的逆操做,好比3进制转换为十进制时候是:1*3^3+0*3^2+0*3^1+1*3^0 ,咱们转换一下是((1*3+0)*3+0)*3+1,如此和10进制转换3进制的时候逆向操做。


3、小数

若是前面的理解了,小数就能够很容易理解了,咱们仍是先从10进制来看。好比十进制12.34,咱们看小数后面十分位部分.3,表示把1分为10份只取3份,.04百分位部分是把1分为100份,取4份。那么咱们换成公式:


12.34=1*10^1+2+3*(1/10)+4*(1/100)
     =1*10^1+2+3*10^-1+4*10^-2复制代码

咱们看到小数部分仍是以进制为底数,不过指数部分采用了负数,点的左边的位的指数是位的正幂,点数的右边是位的指数负幂。理解了这个,其余进制的小数部分也就了解, 它们是相同的,好比二进制1001.101:

有了这个理解,咱们后续的浮点数就比较好理解了,IEEE浮点表示浮点数,也是基于这种方式,只是定义了些规范,后续咱们会详细了解。


4、移位操做

常见的移位操做有三种:左移,逻辑右移,算术右移。

移动操做

操做
参数x [01100011] [10010101]
x<<4 [00110000] [01010000]
x>>4(逻辑右移) [00000110] [00001001]
x>>4(算术右移) [00000110] [11111001]


一、左移

x向左移动k位,会丢弃最高的k位,并在右端补k个0,也就是常说的当前值乘以2的k次方。为什么是乘以2的k次方?咱们看10进制的时候,某数乘以10,就是在末尾增长1个0 ,由此咱们能够联想到,二进制左移一位(末尾加一个0)至关于乘以2,这个结论广泛存在于全部进位制中:k进制数的末尾加个0,至关于该数乘以k。

咱们从图中能够看到,左移动一位,就至关于进位制展开式的每一个指数都加1,如此移动一位,就至关于当前数(1*2^5+1*2^1+!*2^0)*2^1=1*2^6+1*2^2+1*2^1


二、右移

理解了左移的原理,右移动的原理也是相同的,右移k位=进位制展开式的每一个指数都减k,也就是当前数除以进制的k次方。惟一不一样的是分为逻辑右移和算术右移。

逻辑右移就是无符号移位,右移几位,就在左端补几个0,好比上边 Varint中每次右移7位,相应的当前数高位就会补充7个0。

算术右移动是有符号移位,和逻辑右移不一样的是,算术右移是在左端补k个最高有效位的值,如此看着有些奇特,但对有符号整数数据的运算很是有用。咱们知道有符号的数,首位字节,是用来表示数字的正负。负数采用补码形式来存储,好比[11100110],10进制是-26,算术右移1位以后[11110011],10进制是-13,如若不是补最高有效位的值1而是补作事0的话,右移以后就变成正数了。


5、字节序

单个字节并无字节序的问题,当一个数据须要多个字节存储的时候,就会牵扯到这样的问题,这个数据的地址是什么,存储器中如何排列这些字节,是高位地址存最高有效位,仍是低位地址存最高有效位。

好比一个int类型的变量,它的地址是使用字节中最小的地址,好比在存储器上的位置是0x10一、0x10二、0x103,它的地址是0x101,如果这个数据是一个w位的整数,位表示为[x(w-1),x(w-2)....,x1,x0],那么其中x(w-1)是最高有效位,x0是最低有效位,w如果8的倍数,位被分组成字节,那么最高有效字节是[x(w-1)...x(w-8)],最低有效字节是[x7,x6...x0]。这个也能够成为物理顺序,和咱们普通人理解的存储顺序预期相符合,好比十进制也是高位(百位,10位)在地位(个位)前面。


一、小端法(little endian)

若是字节的逻辑顺序与物理顺序相反,也就是w的最低有效字节在前面[x7,x6....x0],最高有效字节[x(w-1)...x(w-8)]在后面,此时成为小端法(little endian)。多数intel兼容机都采用这种规则。

二、大端法(big endian)

若是字节的逻辑顺序与物理顺序相同,也就是w的最低有效字节[x(w-1)...x(w-8)]在前面,最高有效字节[x7,x6....x0]在后面,称为大端法(big endian),大多数IBM和SunMicrosystems的机器都是采用这种规则。


好比一个十六进制数:0x01234567,咱们用大端小端法看他们在存储器上的位置。

咱们能够看到大端法是比较符合咱们习惯的,高位在前地位在后。

上述Varint的算法,是采用小端法来存储字节顺序的。

buffer[position++] = (byte) ((value & 0x7F) | 0x80);复制代码

每次都是获取当前数据的后7个字节存储到数据流buffer里面,也就是低位字节放在buffer字节数组的前面。

----------------------------------------------end------------------------------------------------

扫描关注更多,关注我的成长和技术学习,期待用本身的一点点改变,带给你一些启发及感悟。

相关文章
相关标签/搜索