優秀應用算法都大量用到位運算,而位運算在工作中很少用到,大多數教材只會教大家二進制和十進制如何互換,都是死記硬背式的,并沒有去講解真正含義,換一個進制之后,依然不會,我們回到最根本的一些計數方法上,從10進制來推算,希望用一種更簡單的方式介紹其原理。
我們來看上一篇的一個Varint算法,這個算法的目的是為了令一個整型占用更少的字節,比如小于127的數字,只需占用一個字節即可,小于16384的數字,采用2個字節即可。算法如下:
我們來看看具體圖例:
我們看到在小于2097153期間,占用空間會小于4個字節,這個優勢還比較明顯,不過也有弊端,比如超過268435456之后會有占用5個字節,考慮到大多數情況下,并不會應用到這么大的數字,優化空間方面還是不錯的。
通過上述算法實現,我發現優秀的應用算法都會大量用到了位運算,而位運算在工作中卻很少用到。位運算速度要快于整數運算的,特別是整數乘法,需要10個或者更多個時鐘,若果采用移位操作一個或者2個時鐘就夠了,不過由于我們常采用十進制來進行算術運算,對二進制的位運算不夠熟悉,閱讀起來會比較耗費精力,所以借助上述算法實現,我們分析一下位運算的優勢以及應用,從而更好的理解二進制。上述代碼中,有運用到移位操作,位運算,字節序等相關知識點,我們一一分析。
進位制
我們知道,計算機的存儲和處理的信息都是以二進制的,雖然在編寫程序的期間數運算還是采用10進制表示,但到機器執行的時候,還會以2進制來進行處理。對于有10個指頭的人來說,熟知10進制是很自然的事情,你看教小孩子數學的時候,都是先從數指頭開始的,那么若是我們只有2個指頭,是不是我們現在會更好理解二進制了?
其他進制轉換10進制
大多數教材會教大家二進制和十進制如何互換,但多數說都是死記硬背式的,并沒有去講解真正含義,換一個進制之后,依然不會,我們回到最根本的一些計數方法上,從10進制來推算。比如我們看一個數字1001,采用十進制表示是:1x10^3+0*10^2+0*10^1+1*10^0。首先從右往左,我們可以看成是從低位到高位,每高一位,指數+1,其次10進制是以10為底數,其三這個公式是采用10進制算術進行計算的(用什么進制算出答案就相當于把當前進制轉換為了什么進制了)。這個方式適合所有的進制轉換,理解了這個,后續的進制轉換都會很容易理解。
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。
那為何不采用其他進制來計算?采用其他進制計算,那么其他進制的乘法口訣你的熟練一遍了,比如10進制的99乘法口訣,你用其他進制的乘法口訣得自己來演繹一遍了,如此這個和我們的常用習慣有些相駁,換算起來會比較慢,所以一般采用十進制與其它進制互轉或者作為中間步驟來處理。
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進制的時候逆向操作。
小數
如果前面的理解了,小數就可以很容易理解了,我們還是先從10進制來看。比如十進制12.34,我們看小數后面十分位部分.3,表示把1分為10份只取3份,.04百分位部分是把1分為100份,取4份。那么我們換成公式:
我們看到小數部分還是以進制為底數,不過指數部分采用了負數,點的左邊的位的指數是位的正冪,點數的右邊是位的指數負冪。理解了這個,其他進制的小數部分也就了解, 它們是相同的,比如二進制1001.101:
有了這個理解,我們后續的浮點數就比較好理解了,IEEE浮點表示浮點數,也是基于這種方式,只是定義了些規范,后續我們會詳細了解。
移位操作
常見的移位操作有三種:左移,邏輯右移,算術右移。
移動操作
左移
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的話,右移之后就變成正數了。
字節序
單個字節并沒有字節序的問題,當一個數據需要多個字節存儲的時候,就會牽扯到這樣的問題,這個數據的地址是什么,存儲器中如何排列這些字節,是高位地址存最高有效位,還是低位地址存最高有效位。
比如一個int類型的變量,它的地址是使用字節中最小的地址,比如在存儲器上的位置是0x101、0x102、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和Su你Microsystems的機器都是采用這種規則。
比如一個十六進制數:0x01234567,我們用大端小端法看他們在存儲器上的位置。
我們可以看到大端法是比較符合我們習慣的,高位在前地位在后。
上述Varint的算法,是采用小端法來存儲字節順序的。
每次都是獲取當前數據的后7個字節存儲到數據流buffer里面,也就是低位字節放在buffer字節數組的前面。
----------------------------------------------end------------------------------------------------