大家或許還記得 Quake III 裡面的一段有如天書般的代碼
,其中用到的神秘常量 0x5F3759DF 究竟是怎麼一回事,著實讓不少人傷透了腦筋。今天,我見到了一段同樣詭異的代碼。
下面這個位元運算小技巧可以迅速給出一個數的二進位表達中末尾有多少個 0 。比如, 123 456 的二進位表達是 1 11100010 01000000 ,因此這個程式給出的結果就是 6 。
unsigned int v; // find the number of trailing zeros in 32-bit v
int r; // result goes here
static const int MultiplyDeBruijnBitPosition[32] =
{
0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];
熟悉位元運算
的朋友們
可以認出, v & -v 的作用就是取出右起連續的 0 以及首次出現的 1 。當 v = 123 456 時, v & -v
就等於 64 ,即二進位的 1000000 。怪就怪在,這個 0x077CB531 是怎麼回事?數組
MultiplyDeBruijnBitPosition 又是什麼玩意兒呢?
這還得從 0x077CB531 本身的一個性質開始說起。把這個常數寫成 32 位二進位,可以得到
00000111011111001011010100110001
這個 01 串有一個無比牛 B 的地方:如果把它看作是迴圈的,它正好包含了全部 32 種可能的 5 位 01
串,既無重複,又無遺漏!其實,這樣的 01 串並不稀奇,因為構造這樣的 01 串完全等價於尋找一個有向圖中的 Euler
迴路。如,構造一個包含 16 個頂點的圖,頂點分別命名為 0000, 0001, 0010, …, 1111 。如果某個點的後 3
位,正好等於另一個點的前 3 位,就畫一條從前者出髮指向後者的箭頭。也就是說,只要兩個頂點上的數滿足 abcd 和 bcde 的關係( a 、 b
、 c 、 d 、 e 可能代表相同的數字),就從 abcd 出發,連一條到 bcde 的路,這條路就記作 abcde
。注意,有些點之間是可以相互到達的(比如 1010 和 0101 ),有些點甚至有一條到達自己的路(比如 0000 )。
構造一個字串使其包含所有可能的 5 位 01 子串,其實就相當於沿著箭頭在中遊走的過程。不妨假設字串以 0000
開頭。如果下一個數字是 1 ,那麼 00001 這個子串就被包含了,同時最新的 4 位元就變成了 0001 ;但若下一個數字還是 0 ,那麼
00000 就被包含了進來,最新的 4 個數仍然是 0000 。從圖上看,這無非是一個從 0000 點出發走了哪條路的問題:你是選擇了沿
00001 這條路走到了 0001 這個點,還是沿著 00000 這條路走回了 0000
這個點。同理,每添加一個數字,就相當於沿著某條路走到了一個新的點,路上所寫的 5 位元就是剛被考慮到的 5
位元。我們的目的便是既無重複又無遺漏地遍曆所有的路。顯然圖中的每個頂點入度和出度都是 2 ,因此這個圖一定存在 Euler
迴路,我們便能輕易構造出一個滿足要求的 01 串了。這樣的 01 串就叫做 De Bruijn 序列。
De Bruijn 序列在這裡究竟有什麼用呢?它的用途其實很簡單,就是為 32 種不同的情況提供了一個唯一索引。比方說, 1000000 後面有 6 個 0 ,將 1000000 乘以 0x077CB531 ,就得到
00000111011111001011010100110001
-> 11011111001011010100110001000000
相當於把 De Bruijn 序列左移了 6 位。再把這個數右移 27 位,就相當於提取出了這個數的頭 5 位:
11011111001011010100110001000000
-> 11011
由於 De Bruijn 序列的性質,因此當輸入數位末尾 0 個數不同時,最後得到的這個 5 位元也不同。而數組
MultiplyDeBruijnBitPosition 則相當於一個字典的功能。 11011 轉回十進位是 27 ,於是我們查一查
MultiplyDeBruijnBitPosition[27] ,程式即返回 6 。
注意到當輸入數位末尾 0 個數超過 27 個時,程式也是正確的,因為左移時低位正好是用 0 填充的。
這段神一般的代碼取自 http://graphics.stanford.edu/~seander/bithacks.html
,歡迎大家前去圍觀。