今天在leetcode上遇到了 137. Single Number II 這道題:
給定一個非空整數數組,除了某個元素只出現一次以外,其余每個元素均出現了三次。找出那個只出現了一次的元素。(Given a non-empty array of integers, every element appears three times except for one, which appears exactly once. Find that single one.)
Note: 你的算法應該具有線性時間復雜度。 你可以不使用額外空間來實現嗎?(our algorithm should have a linear runtime complexity. Could you implement it without using extra memory?)
Examples:
Input: [0,1,0,1,0,1,99]
Output: 99
剛開始看到這道題時候,我是略微欣喜的,因為腦子里蹦出的想法應該就是用位異或的方法解決。然而事情並沒有那么簡單。在草稿紙上模糊了快一個小時候,我點開了Discuss,進入了投票數最高的回答:
這一點開不得了,我的表情是這樣的😳(睜大雙眼的臉),里面只有這寥寥幾行代碼:
public int singleNumber(int[] A) {
int ones = 0, twos = 0;
for(int i = 0; i < A.length; i++){
ones = (ones ^ A[i]) & ~twos;
twos = (twos ^ A[i]) & ~ones;
}
return ones;
}
上面的代碼,如果你看兩眼就明白。你可以大大的鄙視我了,這篇文章可能不適合你,但是我們還是交個朋友吧🤝
開玩笑,言歸正傳。這時我點開了另一個回答An General Way to Handle All this sort of questions.,簡直如沐春風。該方法采用了數字邏輯電路里的計算,來解決諸如此類的問題。哈哈,好歹我本科也是學過數電這門課的。這篇回答短小精悍,我也理解了半天,下面主要介紹我的理解:
(正經臉)
這個方法核心思想是建立一個記錄狀態的變量,此方法適用於其他所有元素出現K次,求唯一一個元素出現M次的問題(every one occurs K times except one occurs M times)。對於leetcode137這個問題,K=3,M=1。
我們先討論K=2的情況,我們可以用所有元素做位異或的方法來得到只出現一次的數,那是因為出現兩次的數都通過異或把他們的所有位都置0了。對於K=2,每個數的每一位,只有兩種情況(1或0),我們可以列出這幾種情況:
current | incoming | next |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
上面的真值表對於每個數的每一位來說,由於異或具有交換律,所以我們可以看成兩兩相同的數的每一位做異或,自然都變成0。剩下一個數的每一位只能跟前面異或完的0做異或,得到的就是他本身。
現在討論K=3的情況。如果能有一種“類異或”的運算,使得3個相同的數做了這種位運算后變成0就好了,這不就跟上面的情況一樣了嘛!哈哈,請叫我們數學家。
我們要找的就是這種“類異或”的位運算。由於K=3,要進行3次位運算后這一位才為0,那么我們是不是可以把一個數的每一位看成有三個狀態呢?嗯,可以試試。
其實用三進制應該是可以解決的,但是對於代碼來說實在是不好理解。那么我們只能人為的用二進制定義每一位的這三種狀態了。根據香農第一定理,3種狀態需要兩位二進制位表示(哈哈😃,原諒我故作玄虛)。我們用(00,01,10)
來分別表示每個數每一位的這三種狀態,且定義如下真值表:(該真值表的運算表示為₸)
current(a, b) | incoming(c) | next(a, b) |
---|---|---|
0, 0 | 0 | 0, 0 |
0, 1 | 0 | 0, 1 |
1, 0 | 0 | 1, 0 |
0, 0 | 1 | 0, 1 |
0, 1 | 1 | 1, 0 |
1, 0 | 1 | 0, 0 |
來理解一下上面這個真值表,每一位用虛擬的兩位二進制(a, b)表示。假設我們把每一位的初始狀態都定為(a', b'),如果接下來進行₸運算的incoming是三個一樣的數,那么第三次₸運算后的結果必定還是(a', b')。
下面就要用到《數字邏輯電路》的知識了:根據真值表寫出邏輯式。
(其實不難:對於a,把next中a=1對應的行組合選出來,對於每一個組合,凡取值為1的變量寫成原變量,取值為0的變量寫成反變量,各變量相乘后得到一個乘積項;最后,把各個組合對應的乘積項相加,就得到了相應的邏輯表達式。對於b同理)
所以:
根據這兩個邏輯式寫出相應的python代碼:
a = (a&~b&~c)|(~a&b&c)
b = (~a&b&~c)|(~a&~b&c)
ps:這三種狀態我們定義00
表示真實位0
, 01
和10
表示真實位1
,所以有如下映射關系:
01 10 => 1
00 => 0
所以,對於最后的結果,我們只需要return a|b
即可得到該位是0還是1.
最后的python代碼:
class Solution:
def singleNumber(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
a = 0
b = 0
for c in nums:
a, b = (a&~b&~c)|(~a&b&c), (~a&b&~c)|(~a&~b&c)
return a|b
但是,上述代碼放在leetcode中,只beats掉了30%的人。幾個原因吧:
- 邏輯式可以化簡。
- 確實還有對於這道題更簡單但是不通用的方法
不過,現在你已經可以解決所有類似的題目了,驚喜吧!🇨🇳