一道位運算的演算法題

發表於2014-10-15

最近遇到這樣一道演算法題:

Given an array of integers, every element appears three times except for one. Find that single one.

一組整數,除了一個只出現一次以外,其他每個整數都恰好出現三次,要尋找那個特殊的整數。

似曾相識

首先,它讓我想起了另外一道類似的題目,如果把上面的“恰好三次”,改成“恰好兩次”,尋找那個特殊的整數,又該怎麼解?

那樣的話,我希望找到一個方法,讓兩個相同的數進行運算以後,能夠泯滅掉,這樣所有的數進行運算,剩下的值就是那個特殊的數。恰好有這樣的方法,這個方法就是“異或”:

通用演算法

“恰好兩次”恰好有“異或”來解,現在“恰好兩次”變成了“恰好三次”,推廣一點說,如果是“恰好N次”,該怎麼解?

通用的演算法中,用一個HashMap可以得到複雜度近似為n的解法,key為數字本身,value計數,到三次的時候delete掉這個entry,迴圈完成以後整個HashMap中剩下的就是那個特殊的整數了。這個解法普普通通,沒有敘述的必要。這個方法可以保證“恰好N次”一樣解決。這個演算法很簡單,就不寫出來了。

另外一個思路,藉由位操作,對於整數32位,對於每一位,整個數列的數加起來去取3的餘數,就是那個特殊的數在該位上的值。這個方法也可以保證“恰好N次”一樣能夠被解決:

關於補碼

但是,我在一開始實現這個演算法的時候,在上面程式碼中②的位置,我漏掉了val<0的情況,因為第一印象告訴我,一個正整數去與上一個掩碼數,會得到一個正整數。但是這是錯誤的印象。比如在引數A等於{ -1, -1, -2, -1 }的時候,漏掉val<0的結果等於一個荒唐的2147483646。

這是為什麼呢?

因為負數在記憶體中是以補碼方式存放的,第一位最高位是符號位,0表示正數,1表示負數,僅當表示負數的時候,餘下的31位等於那個數的數值每一位都取反,然後加1。例如-1,這32位數是:

32位整數的範圍是從-2147483648到2147483647,為何負值比正值能表示的數多一個,就在於這個“加一”(表示0的時候符號位是0,相當於表示0的時候佔用了正數的表示法)。

所以,如果漏掉了上面程式碼中val<0的情況,在執行到i=31的迴圈的時候,掩碼mask即1<<i是-2147483648,因為它把符號位給變成了1,後面都是0:

所以這個數也是補碼錶示的負數中,最特殊的一個。

那為什麼上面說漏掉val<0之後算錯的結果是2147483646呢?

這個實際要求解的數-2在記憶體中的表示是這樣的:

由於前面說到的,符號位錯誤了,變成了正數,而正數的表示法可不是補碼錶示,所以得出了2147483646這個數。

藉助兩個數的每一位儲存資訊

下面這個方法稍微有點難理解,而且很容易寫錯。需要兩個數(one和accumulation),因為一個數在每一位上面無法存放超過兩次同樣的數出現的資訊。每次迴圈中,需要先標記出現,然後再清零出現過三次的標誌位。最終one留下的每一位都是無法清零的,即出現次數不是3的整數倍的。

其實,這道題還有許多其他做法,既包括利用位運算的其他做法,也有那種“先排序,然後再尋找特殊數”這樣突破常規想法的解法(其實我覺得先排序這樣的做法很好啊,雖然複雜度稍微高一些(取決於排序的時間複雜度了),但是清晰,而且通用性更好。方法雖然簡單,但是我大概受到的教條式思維太嚴重了,這樣的方法根本沒有想到……)。

相關文章