首先感謝Nibnat的提醒,昨天最后一個(gè)檢查2的冪的例子寫錯(cuò)了,正確的方法應(yīng)該是
return ( Number & ( Number - 1 )) ==0;
這個(gè)問題在二進(jìn)制表示下比較好理解,2的次冪在二進(jìn)制下的特點(diǎn)很明顯:只有一個(gè)bit是1,其他bit都是0,于是任務(wù)轉(zhuǎn)換成怎么檢查一個(gè)二進(jìn)制數(shù)是由一個(gè)’1’和好多個(gè)’0’組成的。理解上面的運(yùn)算,需要把注意力放在Number中所有值為’1’的bit的最低位上(我們稱它為最低’1’bit位)。根據(jù)定義,最低’1’bit位左邊可能是0-1組成的任意序列,右邊只能是若干個(gè)0組成的序列。2的冪和非2的冪的區(qū)別是非2冪的最低’1’bit左邊一定有至少一個(gè)’1’bit,而2冪的最低’1’bit位左邊全是0。
現(xiàn)在看看上面的運(yùn)算做了什么事情,Number-1之后,最低’1’bit位的值由1變成0,原來最低’1’bit位左邊的序列保持不變,右邊的0序列變成1序列。跟Number按位進(jìn)行與運(yùn)算之后,Number最低’1’bit位右邊的序列保持不變,最低’1’bit位及其左邊的序列全變成0.于是最終結(jié)果相當(dāng)于檢查Number的最低’1’bit位左邊還有沒有別的’1’bit了。
0是特殊情況,按照數(shù)學(xué)定義,0不是2的冪,不過上面的方法會(huì)輸出TRUE,需要用別的方法檢查一下。
既然提到最低’1’bit位這個(gè)概念了,今天就趁這個(gè)機(jī)會(huì)介紹另一個(gè)與之相關(guān)的,我特別喜歡的例子。直接上代碼先:
x;
y = x & -x;
c = x + y;
x = (((x ^ c) >> 2) / y) | c;
這個(gè)例子比較復(fù)雜,容我一步一步解開大家心里的疑問。
問題一:y是什么?
需要先說明一下,計(jì)算機(jī)中使用補(bǔ)碼來存儲(chǔ)負(fù)數(shù),所謂補(bǔ)碼,就是按位取反之后再加1,所以-x其實(shí)相當(dāng)于~x+1。x按位取反之后,原來最低’1’bit位變成了0,右邊全變成1了。加1就如同推到了第一塊多米諾骨牌一樣,從最右邊開始把一串1又變回了0,骨牌效應(yīng)到最低’1’bit這里停止,把它從0又拽回來變成1了。總結(jié)一下,-x相當(dāng)于只把最低’1’bit左邊內(nèi)容變反,和x按位與運(yùn)算之后,最低’1’bit為1,其他地方全部是0。所以y其實(shí)是x的最低’1’bit。
問題二:c是什么?
前面解釋過了,y其實(shí)是x的最低’1’bit,那么x和自己的最低’1’bit位相加結(jié)果是什么呢?還是用多米諾骨牌來解釋這個(gè)過程。把x中的1想象成多米諾骨牌,0想象成空地,加1就相當(dāng)于推到一個(gè)骨牌,只要下一位還是1,那么就會(huì)一直向高位進(jìn)位,直到遇到一個(gè)0的時(shí)候才會(huì)停止進(jìn)位。x可以理解為被0隔開的一段段連續(xù)的1,c就是把x最右端的一片1推到后再來一次進(jìn)位的結(jié)果。
問題三:x^ c是什么?
異或運(yùn)算的規(guī)則是二者不同則結(jié)果為真,那我們來看看x和c在哪些bit上會(huì)取不同的值。依然把x理解為被0隔開的一段段連續(xù)的1,c是把x最右端的一片1推到成0之后的結(jié)果,那么可以確定x和c不一樣的地方,就是x最右端的那一串連續(xù)的1,所以x ^ c的結(jié)果其實(shí)就是x最右端的一串1外加一次進(jìn)位。
((x ^ c) >> 2)比較好理解,就是右移兩位。
問題四:除y做什么?
前面第一個(gè)分析過,y是x的最低’1’bit。y只有這一個(gè)1,其他地方都是0,所以y是2的冪!計(jì)算機(jī)中除以2的冪,等價(jià)于右移運(yùn)算。于是((x^ c) >> 2)|y的作用相當(dāng)于把x^ c右移了2+log(y)bit,實(shí)際是把x最右端的一串1使勁地往右移動(dòng),直到把一個(gè)1都擠出去了。
問題五:為什么要和c按位或?
第二個(gè)問題中分析了c是把x最右端的一串1推到變成0,然后向前進(jìn)一位的結(jié)果,而((x ^ c) >> 2) / y是x最右端的一串1使勁地往右移動(dòng)直到擠出去一位的結(jié)果,他倆按位或運(yùn)算,就是把x最右端的一串1分成兩個(gè)部分,第一個(gè)bit往前進(jìn)一位,其余的被壓到最右端去。
整個(gè)過程解釋完了。
問題六:這段代碼到底在干啥?
把x理解成若干個(gè)1和0組成的二進(jìn)制字符串,從問題五的答案總可以看出,這段代碼并沒有改變字符1的個(gè)數(shù),而是把最右端的一串連續(xù)的1的位置調(diào)整了一下。仔細(xì)想想可以發(fā)現(xiàn),這是在保證1的個(gè)數(shù)不變的前提下,下一個(gè)最小的x。這個(gè)功能在組合問題中使用的特別多,可以幫我們找到所有從m個(gè)元素中選出n個(gè)元素的方案。
這段代碼是計(jì)算機(jī)界的大牛,Hacker界的鼻祖BillGosper發(fā)現(xiàn)的,因此也被稱為G函數(shù)。我們最后用幾個(gè)例子來展示一下其中的微妙