轉於http://www.matrix67.com/blog/archives/263
去年年底寫的關於位運算的日誌是這個blog裡少數大受歡迎的文章之一,很多人都希望我能不斷完善那篇文章。後來我看到了不少其它的資料,學習到了更多關於位運算的知識,有了重新整理位運算技巧的想法。從今天起我就開始寫這一系列位運算講解文章,與其說是原來那篇文章的follow-up,不如說是乙個remake。當然首先我還是從最基礎的東西說起。
什麼是位運算?
程式中的所有數在計算機記憶體中都是以二進位制的形式儲存的。位運算說穿了,就是直接對整數在記憶體中的二進位制位進行操作。比如,and運算本來是乙個邏輯運算子,但整數與整數之間也可以進行and運算。舉個例子,6的二進位制是110,11的二進位制是1011,那麼6 and 11的結果就是2,它是二進位制對應位進行邏輯運算的結果(0表示false,1表示true,空位都當0處理):
110and 1011
----------
0010 --> 2
由於位運算直接對記憶體資料進行操作,不需要轉成十進位制,因此處理速度非常快。當然有人會說,這個快了有什麼用,計算6 and 11沒有什麼實際意義啊。這一系列的文章就將告訴你,位運算到底可以幹什麼,有些什麼經典應用,以及如何用位運算優化你的程式。
pascal和c中的位運算符號
下面的a和b都是整數型別,則:
c語言 | pascal語言
-------+-------------
a & b | a and b
a | b | a or b
a ^ b | a xor b
~a | not a
a << b | a shl b
a >> b | a shr b
注意c中的邏輯運算和位運算符號是不同的。520|1314=1834,但520||1314=1,因為邏輯運算時520和1314都相當於true。同樣的,!a和~a也是有區別的。
各種位運算的使用
=== 1. and運算 ===
and運算通常用於二進位製取位操作,例如乙個數 and 1的結果就是取二進位制的最末位。這可以用來判斷乙個整數的奇偶,二進位制的最末位為0表示該數為偶數,最末位為1表示該數為奇數.
=== 2. or運算 ===
or運算通常用於二進位制特定位上的無條件賦值,例如乙個數or 1的結果就是把二進位制最末位強行變成1。如果需要把二進位制最末位變成0,對這個數or 1之後再減一就可以了,其實際意義就是把這個數強行變成最接近的偶數。
=== 3. xor運算 ===
xor運算通常用於對二進位制的特定一位進行取反操作,因為異或可以這樣定義:0和1異或0都不變,異或1則取反。
xor運算的逆運算是它本身,也就是說兩次異或同乙個數最後結果不變,即(a xor b) xor b = a。xor運算可以用於簡單的加密,比如我想對我mm說1314520,但怕別人知道,於是雙方約定拿我的生日19880516作為金鑰。1314520 xor 19880516 = 20665500,我就把20665500告訴mm。mm再次計算20665500 xor 19880516的值,得到1314520,於是她就明白了我的企圖。
下面我們看另外乙個東西。定義兩個符號#和@(我怎麼找不到那個圈裡有個叉的字元),這兩個符號互為逆運算,也就是說(x # y) @ y = x。現在依次執行下面三條命令,結果是什麼?
x <- x # y
y <- x @ y
x <- x @ y
執行了第一句後x變成了x # y。那麼第二句實質就是y <- x # y @ y,由於#和@互為逆運算,那麼此時的y變成了原來的x。第三句中x實際上被賦值為(x # y) @ x,如果#運算具有交換律,那麼賦值後x就變成最初的y了。這三句話的結果是,x和y的位置互換了。
加法和減法互為逆運算,並且加法滿足交換律。把#換成+,把@換成-,我們可以寫出乙個不需要臨時變數的swap過程(pascal)。
procedure swap(var a,b:longint);
begin
a:=a + b;
b:=a - b;
a:=a - b;
end;
好了,剛才不是說xor的逆運算是它本身嗎?於是我們就有了乙個看起來非常詭異的swap過程:
procedure swap(var a,b:longint);
begin
a:=a xor b;
b:=a xor b;
a:=a xor b;
end;
=== 4. not運算 ===
not運算的定義是把記憶體中的0和1全部取反。使用not運算時要格外小心,你需要注意整數型別有沒有符號。如果not的物件是無符號整數(不能表示負數),那麼得到的值就是它與該型別上界的差,因為無符號型別的數是用$0000到$ffff依次表示的。下面的兩個程式(僅語言不同)均返回65435。
var
a:word;
begin
a:=100;
a:=not a;
writeln(a);
end.
#include
int main()
如果not的物件是有符號的整數,情況就不一樣了,稍後我們會在「整數型別的儲存」小節中提到。
=== 5. shl運算 ===
a shl b就表示把a轉為二進位制後左移b位(在後面添b個0)。例如100的二進位制為1100100,而110010000轉成十進位制是400,那麼100 shl 2 = 400。可以看出,a shl b的值實際上就是a乘以2的b次方,因為在二進位制數後添乙個0就相當於該數乘以2。
通常認為a shl 1比a * 2更快,因為前者是更底層一些的操作。因此程式中乘以2的操作請盡量用左移一位來代替。
定義一些常量可能會用到shl運算。你可以方便地用1 shl 16 - 1來表示65535。很多演算法和資料結構要求資料規模必須是2的冪,此時可以用shl來定義max_n等常量。
=== 6. shr運算 ===
和shl相似,a shr b表示二進位制右移b位(去掉末b位),相當於a除以2的b次方(取整)。我們也經常用shr 1來代替div 2,比如二分查詢、堆的插入操作等等。想辦法用shr代替除法運算可以使程式效率大大提高。最大公約數的二進位制演算法用除以2操作來代替慢得出奇的mod運算,效率可以提高60%。
位運算的簡單應用
最後這乙個在樹狀陣列中會用到。
pascal和c中的16進製表示
pascal中需要在16進製制數前加$符號表示,c中需要在前面加0x來表示。這個以後我們會經常用到。
整數型別的儲存
我們前面所說的位運算都沒有涉及負數,都假設這些運算是在unsigned/word型別(只能表示正數的整型)上進行操作。但計算機如何處理有正負符號的整數型別呢?下面兩個程式都是考察16位整數的儲存方式(只是語言不同)。
var
a,b:integer;
begin
a:=$0000;
b:=$0001;
write(a,' ',b,' ');
a:=$fffe;
b:=$ffff;
write(a,' ',b,' ');
a:=$7fff;
b:=$8000;
writeln(a,' ',b);
end.
#include
int main()
兩個程式的輸出均為0 1 -2 -1 32767 -32768。其中前兩個數是記憶體值最小的時候,中間兩個數則是記憶體值最大的時候,最後輸出的兩個數是正數與負數的分界處。由此你可以清楚地看到計算機是如何儲存乙個整數的:計算機用$0000到$7fff依次表示0到32767的數,剩下的$8000到$ffff依次表示-32768到-1的數。32位有符號整數的儲存方式也是類似的。稍加注意你會發現,二進位制的第一位是用來表示正負號的,0表示正,1表示負。這裡有乙個問題:0本來既不是正數,也不是負數,但它占用了$0000的位置,因此有符號的整數型別範圍中正數個數比負數少乙個。對乙個有符號的數進行not運算後,最高位的變化將導致正負顛倒,並且數的絕對值會差1。也就是說,not a實際上等於-a-1。這種整數儲存方式叫做「補碼」。
位運算簡介及實用技巧(一) 基礎篇
去年年底寫的關於位運算的日誌是這個blog裡少數大受歡迎的文章之一,很多人都希望我能不斷完善那篇文章。後來我看到了不少其它的資料,學習到了更多關於位運算的知識,有了重新整理位運算技巧的想法。從今天起我就開始寫這一系列位運算講解文章,與其說是原來那篇文章的follow up,不如說是乙個remake。...
位運算簡介及實用技巧(一) 基礎篇
什麼是位運算?程式中的所有數在計算機記憶體中都是以二進位制的形式儲存的。位運算說穿了,就是直接對整 數在記憶體中的二進位制位進行操作。比如,and運算本來是乙個邏輯運算子,但整數與整數之間也可以進行and運算。舉個例子,6的二進位制是110,11的二 進製是1011,那麼6 and 11的結果就是2...
位運算簡介及實用技巧(一) 基礎篇
去年年底寫的關於位運算的日誌是這個blog裡少數大受歡迎的文章之一,很多人都希望我能不斷完善那篇文章。後來我看到了不少其它的資料,學習到了更多關於位運算的知識,有了重新整理位運算技巧的想法。從今天起我就開始寫這一系列位運算講解文章,與其說是原來那篇文章的follow up,不如說是乙個remake。...