位運算簡介及實用技巧(一) 基礎篇

2021-04-21 10:51:58 字數 3953 閱讀 8534

什麼是位運算?

程式中的所有數在計算機記憶體中都是以二進位制的形式儲存的。位運算說穿了,就是直接對整 數在記憶體中的二進位制位進行操作。比如,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。...

位運算簡介及實用技巧(一) 基礎篇

轉於http www.matrix67.com blog archives 263 去年年底寫的關於位運算的日誌是這個blog裡少數大受歡迎的文章之一,很多人都希望我能不斷完善那篇文章。後來我看到了不少其它的資料,學習到了更多關於位運算的知識,有了重新整理位運算技巧的想法。從今天起我就開始寫這一系列...

位運算簡介及實用技巧(一) 基礎篇

去年年底寫的關於位運算的日誌是這個blog裡少數大受歡迎的文章之一,很多人都希望我能不斷完善那篇文章。後來我看到了不少其它的資料,學習到了更多關於位運算的知識,有了重新整理位運算技巧的想法。從今天起我就開始寫這一系列位運算講解文章,與其說是原來那篇文章的follow up,不如說是乙個remake。...