(一):基礎(chǔ)篇 Matrix67: The Aha Moments
位運(yùn)算簡(jiǎn)介及實(shí)用技巧(二):進(jìn)階篇(1)
位運(yùn)算簡(jiǎn)介及實(shí)用技巧(三):進(jìn)階篇(2)
位運(yùn)算簡(jiǎn)介及實(shí)用技巧(四):實(shí)戰(zhàn)篇
八皇后問(wèn)題,是一個(gè)古老而著名的問(wèn)題,是回溯算法的典型案例察署。該問(wèn)題是國(guó)際西洋棋棋手馬克斯·貝瑟爾于1848年提出:在8×8格的國(guó)際象棋上擺放八個(gè)皇后葵擎,使其不能互相攻擊,即任意兩個(gè)皇后都不能處于同一行幕庐、同一列或同一斜線(xiàn)上,問(wèn)有多少種擺法。 高斯認(rèn)為有76種方案吆倦。1854年在柏林的象棋雜志上不同的作者發(fā)表了40種不同的解,后來(lái)有人用圖論的方法解出92種結(jié)果坐求。計(jì)算機(jī)發(fā)明后蚕泽,有多種計(jì)算機(jī)語(yǔ)言可以解決此問(wèn)題
怎樣對(duì)10億個(gè)數(shù)字快速去重?——淺析位圖數(shù)據(jù)結(jié)構(gòu)及其應(yīng)用
怎樣對(duì)10億個(gè)數(shù)字快速去重桥嗤?——淺析位圖數(shù)據(jù)結(jié)構(gòu)及其應(yīng)用
最近有個(gè)朋友問(wèn)我一個(gè)算法題——
給你幾億個(gè)QQ號(hào)须妻,怎樣快速去除重復(fù)的QQ號(hào)?
可以作如下假定:
QQ號(hào)數(shù)字范圍從0到十億荒吏,即[0, 1000000000),且最多給你10億個(gè)QQ號(hào)绰更,這些QQ號(hào)放在1或多個(gè)文本文件中,格式是每行一個(gè)QQ號(hào)动知。
【測(cè)試用例2:】
我們可以寫(xiě)一個(gè)小程序生成N個(gè)范圍[0, 10億)的數(shù)字,也就是最大的數(shù)是包含9個(gè)9的999999999员辩。
平方根倒數(shù)速算法*是用于快速計(jì)算[圖片上傳中盒粮。。奠滑。(1)](即[圖片上傳中丹皱。。宋税。(2)]的平方根的倒數(shù)摊崭,在此[圖片上傳中。杰赛。呢簸。(3)]需取符合IEEE 754標(biāo)準(zhǔn)格式的32位浮點(diǎn)數(shù))的一種算法。此算法最早可能是于90年代前期由SGI所發(fā)明,后來(lái)則于1999年在《雷神之錘III競(jìng)技場(chǎng)》的源代碼中應(yīng)用根时,但直到2002-2003年間才在Usenet一類(lèi)的公共論壇上出現(xiàn)瘦赫。這一算法的優(yōu)勢(shì)在于減少了求平方根倒數(shù)時(shí)浮點(diǎn)運(yùn)算操作帶來(lái)的巨大的運(yùn)算耗費(fèi),而在計(jì)算機(jī)圖形學(xué)領(lǐng)域蛤迎,若要求取照明和投影的波動(dòng)角度與反射效果确虱,就常需計(jì)算平方根倒數(shù)。
float Q_rsqrt( float number ){
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y;
// evil floating point bit level hacking(對(duì)浮點(diǎn)數(shù)的邪惡位級(jí)hack)
i = 0x5f3759df - ( i >> 1 );
// what the fuck?(這他媽的是怎么回事替裆?)
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) );
// 1st iteration (第一次牛頓迭代)
// y = y * ( threehalfs - ( x2 * y * y ) );
// 2nd iteration, this can be removed(第二次迭代校辩,可以刪除)
return y;
}
斯坦福計(jì)算機(jī)系都整理好了 Bit Twiddling Hacks
整型轉(zhuǎn)成二進(jìn)制有32位,長(zhǎng)整型64位辆童,可以利用這一特性存儲(chǔ)有限元素(1 \leq n \leq 64)的狀態(tài)宜咒,二進(jìn)制上的每一位表示一個(gè)元素狀態(tài),0表示true胸遇,1表示false荧呐。
舉個(gè)實(shí)際業(yè)務(wù)上的栗子:簽到系統(tǒng)
public static int[] days = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9}; public static void main(String[] args) { int state = 0; for (int day : days) { boolean flag = (state >> day & 1) == 1; System.err.println("第" + day + "天的簽到狀態(tài):" + flag); } System.err.println("============"); state |= 1 << days[0];// 第1天 state |= 1 << days[1];// 第2天 state |= 1 << days[2];// 第3天 state |= 1 << days[3];// 第4天 state |= 1 << days[6];// 第7天 for (int day : days) { boolean flag = (state >> day & 1) == 1; System.err.println("第" + day + "天的簽到狀態(tài):" + flag); } }
什么是位運(yùn)算汉形?
程序中的所有數(shù)在計(jì)算機(jī)內(nèi)存中都是以二進(jìn)制的形式儲(chǔ)存的纸镊。位運(yùn)算說(shuō)穿了,就是直接對(duì)整數(shù)在內(nèi)存中的二進(jìn)制位進(jìn)行操作概疆。比如逗威,and運(yùn)算本來(lái)是一個(gè)邏輯運(yùn)算符,但整數(shù)與整數(shù)之間也可以進(jìn)行and運(yùn)算岔冀。舉個(gè)例子凯旭,6的二進(jìn)制是110罐呼,11的二進(jìn)制是1011嫉柴,那么6 and 11的結(jié)果就是2奉呛,它是二進(jìn)制對(duì)應(yīng)位進(jìn)行邏輯運(yùn)算的結(jié)果(0表示False登馒,1表示True陈轿,空位都當(dāng)0處理):
110
AND 1011
———-
0010 –> 2
由于位運(yùn)算直接對(duì)內(nèi)存數(shù)據(jù)進(jìn)行操作,不需要轉(zhuǎn)成十進(jìn)制赠堵,因此處理速度非趁0龋快揍愁。當(dāng)然有人會(huì)說(shuō)莽囤,這個(gè)快了有什么用朽缎,計(jì)算6 and 11沒(méi)有什么實(shí)際意義啊话肖。這一系列的文章就將告訴你最筒,位運(yùn)算到底可以干什么床蜘,有些什么經(jīng)典應(yīng)用邢锯,以及如何用位運(yùn)算優(yōu)化你的程序丹擎。
Pascal和C中的位運(yùn)算符號(hào)
下面的a和b都是整數(shù)類(lèi)型鸥鹉,則:
C語(yǔ)言 | Pascal語(yǔ)言
——-+————-
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中的邏輯運(yùn)算和位運(yùn)算符號(hào)是不同的毁渗。520|1314=1834灸异,但520||1314=1肺樟,因?yàn)檫壿嬤\(yùn)算時(shí)520和1314都相當(dāng)于True疟暖。同樣的俐巴,!a和~a也是有區(qū)別的欣舵。
各種位運(yùn)算的使用
=== 1. and運(yùn)算 ===
and運(yùn)算通常用于二進(jìn)制取位操作缘圈,例如一個(gè)數(shù) and 1的結(jié)果就是取二進(jìn)制的最末位糟把。這可以用來(lái)判斷一個(gè)整數(shù)的奇偶糊饱,二進(jìn)制的最末位為0表示該數(shù)為偶數(shù)颠黎,最末位為1表示該數(shù)為奇數(shù).
=== 2. or運(yùn)算 ===
or運(yùn)算通常用于二進(jìn)制特定位上的無(wú)條件賦值狭归,例如一個(gè)數(shù)or 1的結(jié)果就是把二進(jìn)制最末位強(qiáng)行變成1过椎。如果需要把二進(jìn)制最末位變成0疚宇,對(duì)這個(gè)數(shù)or 1之后再減一就可以了敷待,其實(shí)際意義就是把這個(gè)數(shù)強(qiáng)行變成最接近的偶數(shù)。
=== 3. xor運(yùn)算 ===
xor運(yùn)算通常用于對(duì)二進(jìn)制的特定一位進(jìn)行取反操作举哟,因?yàn)楫惢蚩梢赃@樣定義:0和1異或0都不變妨猩,異或1則取反钠导。
xor運(yùn)算的逆運(yùn)算是它本身牡属,也就是說(shuō)兩次異或同一個(gè)數(shù)最后結(jié)果不變逮栅,即(a xor b) xor b = a。xor運(yùn)算可以用于簡(jiǎn)單的加密侥加,比如我想對(duì)我MM說(shuō)1314520担败,但怕別人知道提前,于是雙方約定拿我的生日19880516作為密鑰。1314520 xor 19880516 = 20665500笨腥,我就把20665500告訴MM拓哺。MM再次計(jì)算20665500 xor 19880516的值,得到1314520脖母,于是她就明白了我的企圖士鸥。
下面我們看另外一個(gè)東西。定義兩個(gè)符號(hào)#和@(我怎么找不到那個(gè)圈里有個(gè)叉的字符)镶奉,這兩個(gè)符號(hào)互為逆運(yùn)算础淤,也就是說(shuō)(x # y) @ y = x≌阜牛現(xiàn)在依次執(zhí)行下面三條命令,結(jié)果是什么鸽凶?
x <- x # y
y <- x @ y
x <- x @ y
執(zhí)行了第一句后x變成了x # y币砂。那么第二句實(shí)質(zhì)就是y <- x # y @ y,由于#和@互為逆運(yùn)算凑兰,那么此時(shí)的y變成了原來(lái)的x音半。第三句中x實(shí)際上被賦值為(x # y) @ x彻桃,如果#運(yùn)算具有交換律耗溜,那么賦值后x就變成最初的y了。這三句話(huà)的結(jié)果是,x和y的位置互換了妖异。
加法和減法互為逆運(yùn)算,并且加法滿(mǎn)足交換律订雾。把#換成+谱净,把@換成-李请,我們可以寫(xiě)出一個(gè)不需要臨時(shí)變量的swap過(guò)程(Pascal)乍炉。
procedure swap(var a,b:longint);
begin
a:=a + b;
b:=a - b;
a:=a - b;
end;
好了,剛才不是說(shuō)xor的逆運(yùn)算是它本身嗎祠挫?于是我們就有了一個(gè)看起來(lái)非常詭異的swap過(guò)程:
procedure swap(var a,b:longint);
begin
a:=a xor b;
b:=a xor b;
a:=a xor b;
end;
=== 4. not運(yùn)算 ===
not運(yùn)算的定義是把內(nèi)存中的0和1全部取反涤浇。使用not運(yùn)算時(shí)要格外小心,你需要注意整數(shù)類(lèi)型有沒(méi)有符號(hào)栋盹。如果not的對(duì)象是無(wú)符號(hào)整數(shù)(不能表示負(fù)數(shù))汉额,那么得到的值就是它與該類(lèi)型上界的差蠕搜,因?yàn)闊o(wú)符號(hào)類(lèi)型的數(shù)是用$0000到$FFFF依次表示的啼器。下面的兩個(gè)程序(僅語(yǔ)言不同)均返回65435。
var
a:word;
begin
a:=100;
a:=not a;
writeln(a);
end.
include <stdio.h>
int main()
{
unsigned short a=100;
a = ~a;
printf( "%dn", a );
return 0;
}
如果not的對(duì)象是有符號(hào)的整數(shù)栗精,情況就不一樣了,稍后我們會(huì)在“整數(shù)類(lèi)型的儲(chǔ)存”小節(jié)中提到叮盘。
=== 5. shl運(yùn)算 ===
a shl b就表示把a(bǔ)轉(zhuǎn)為二進(jìn)制后左移b位(在后面添b個(gè)0)秩贰。例如100的二進(jìn)制為1100100,而110010000轉(zhuǎn)成十進(jìn)制是400柔吼,那么100 shl 2 = 400毒费。可以看出愈魏,a shl b的值實(shí)際上就是a乘以2的b次方觅玻,因?yàn)樵诙M(jìn)制數(shù)后添一個(gè)0就相當(dāng)于該數(shù)乘以2。
通常認(rèn)為a shl 1比a * 2更快培漏,因?yàn)榍罢呤歉讓右恍┑牟僮飨濉R虼顺绦蛑谐艘?的操作請(qǐng)盡量用左移一位來(lái)代替。
定義一些常量可能會(huì)用到shl運(yùn)算牌柄。你可以方便地用1 shl 16 – 1來(lái)表示65535畸悬。很多算法和數(shù)據(jù)結(jié)構(gòu)要求數(shù)據(jù)規(guī)模必須是2的冪,此時(shí)可以用shl來(lái)定義Max_N等常量珊佣。
=== 6. shr運(yùn)算 ===
和shl相似蹋宦,a shr b表示二進(jìn)制右移b位(去掉末b位)披粟,相當(dāng)于a除以2的b次方(取整)。我們也經(jīng)常用shr 1來(lái)代替div 2冷冗,比如二分查找守屉、堆的插入操作等等。想辦法用shr代替除法運(yùn)算可以使程序效率大大提高蒿辙。最大公約數(shù)的二進(jìn)制算法用除以2操作來(lái)代替慢得出奇的mod運(yùn)算拇泛,效率可以提高60%。
位運(yùn)算的簡(jiǎn)單應(yīng)用
有時(shí)我們的程序需要一個(gè)規(guī)模不大的Hash表來(lái)記錄狀態(tài)思灌。比如俺叭,做數(shù)獨(dú)時(shí)我們需要27個(gè)Hash表來(lái)統(tǒng)計(jì)每一行、每一列和每一個(gè)小九宮格里已經(jīng)有哪些數(shù)了习瑰。此時(shí)绪颖,我們可以用27個(gè)小于2^9的整數(shù)進(jìn)行記錄。例如甜奄,一個(gè)只填了2和5的小九宮格就用數(shù)字18表示(二進(jìn)制為000010010)柠横,而某一行的狀態(tài)為511則表示這一行已經(jīng)填滿(mǎn)。需要改變狀態(tài)時(shí)我們不需要把這個(gè)數(shù)轉(zhuǎn)成二進(jìn)制修改后再轉(zhuǎn)回去课兄,而是直接進(jìn)行位操作牍氛。在搜索時(shí),把狀態(tài)表示成整數(shù)可以更好地進(jìn)行判重等操作烟阐。這道題是在搜索中使用位運(yùn)算加速的經(jīng)典例子搬俊。以后我們會(huì)看到更多的例子。
下面列舉了一些常見(jiàn)的二進(jìn)制位的變換操作蜒茄。
功能 | 示例 | 位運(yùn)算
———————-+—————————+——————–
去掉最后一位 | (101101->10110) | x shr 1
在最后加一個(gè)0 | (101101->1011010) | x shl 1
在最后加一個(gè)1 | (101101->1011011) | x shl 1+1
把最后一位變成1 | (101100->101101) | x or 1
把最后一位變成0 | (101101->101100) | x or 1-1
最后一位取反 | (101101->101100) | x xor 1
把右數(shù)第k位變成1 | (101001->101101,k=3) | x or (1 shl (k-1))
把右數(shù)第k位變成0 | (101101->101001,k=3) | x and not (1 shl (k-1))
右數(shù)第k位取反 | (101001->101101,k=3) | x xor (1 shl (k-1))
取末三位 | (1101101->101) | x and 7
取末k位 | (1101101->1101,k=5) | x and (1 shl k-1)
取右數(shù)第k位 | (1101101->1,k=4) | x shr (k-1) and 1
把末k位變成1 | (101001->101111,k=4) | x or (1 shl k-1)
末k位取反 | (101001->100110,k=4) | x xor (1 shl k-1)
把右邊連續(xù)的1變成0 | (100101111->100100000) | x and (x+1)
把右起第一個(gè)0變成1 | (100101111->100111111) | x or (x+1)
把右邊連續(xù)的0變成1 | (11011000->11011111) | x or (x-1)
取右邊連續(xù)的1 | (100101111->1111) | (x xor (x+1)) shr 1
去掉右起第一個(gè)1的左邊 | (100101000->1000) | x and (x xor (x-1))
最后這一個(gè)在樹(shù)狀數(shù)組中會(huì)用到唉擂。
Pascal和C中的16進(jìn)制表示
Pascal中需要在16進(jìn)制數(shù)前加$符號(hào)表示,C中需要在前面加0x來(lái)表示檀葛。這個(gè)以后我們會(huì)經(jīng)常用到玩祟。
整數(shù)類(lèi)型的儲(chǔ)存
我們前面所說(shuō)的位運(yùn)算都沒(méi)有涉及負(fù)數(shù),都假設(shè)這些運(yùn)算是在unsigned/word類(lèi)型(只能表示正數(shù)的整型)上進(jìn)行操作屿聋。但計(jì)算機(jī)如何處理有正負(fù)符號(hào)的整數(shù)類(lèi)型呢空扎?下面兩個(gè)程序都是考察16位整數(shù)的儲(chǔ)存方式(只是語(yǔ)言不同)。
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 <stdio.h>
int main()
{
short int a, b;
a = 0x0000;
b = 0x0001;
printf( "%d %d ", a, b );
a = 0xFFFE;
b = 0xFFFF;
printf( "%d %d ", a, b );
a = 0x7FFF;
b = 0x8000;
printf( "%d %dn", a, b );
return 0;
}
兩個(gè)程序的輸出均為0 1 -2 -1 32767 -32768润讥。其中前兩個(gè)數(shù)是內(nèi)存值最小的時(shí)候转锈,中間兩個(gè)數(shù)則是內(nèi)存值最大的時(shí)候,最后輸出的兩個(gè)數(shù)是正數(shù)與負(fù)數(shù)的分界處楚殿。由此你可以清楚地看到計(jì)算機(jī)是如何儲(chǔ)存一個(gè)整數(shù)的:計(jì)算機(jī)用$0000到$7FFF依次表示0到32767的數(shù)撮慨,剩下的$8000到$FFFF依次表示-32768到-1的數(shù)。32位有符號(hào)整數(shù)的儲(chǔ)存方式也是類(lèi)似的。稍加注意你會(huì)發(fā)現(xiàn)甫煞,二進(jìn)制的第一位是用來(lái)表示正負(fù)號(hào)的菇曲,0表示正冠绢,1表示負(fù)抚吠。這里有一個(gè)問(wèn)題:0本來(lái)既不是正數(shù),也不是負(fù)數(shù)弟胀,但它占用了$0000的位置楷力,因此有符號(hào)的整數(shù)類(lèi)型范圍中正數(shù)個(gè)數(shù)比負(fù)數(shù)少一個(gè)。對(duì)一個(gè)有符號(hào)的數(shù)進(jìn)行not運(yùn)算后孵户,最高位的變化將導(dǎo)致正負(fù)顛倒萧朝,并且數(shù)的絕對(duì)值會(huì)差1。也就是說(shuō)夏哭,not a實(shí)際上等于-a-1检柬。這種整數(shù)儲(chǔ)存方式叫做“補(bǔ)碼”。
最后還有兩句話(huà)
Matrix67原創(chuàng)
轉(zhuǎn)貼請(qǐng)注明出處