有趣的二進制

優秀應用算法都大量用到位運算,而位運算在工做中不多用到,因此藉助其算法,咱們看一下位運算的優點以及應用,可是大多數教材只會教你們二進制和十進制如何互換,都是死記硬背式的,並無去講解真正含義,換一個進制以後,依然不會,咱們回到最根本的一些計數方法上,從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------------------------------------------------

掃描關注更多,關注我的成長和技術學習,期待用本身的一點點改變,帶給你一些啓發及感悟。

相關文章
相關標籤/搜索