題目
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:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?
你的算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎?
Example 1:
Input: [2,2,3,2]
Output: 3
Example 2:
Input: [0,1,0,1,0,1,99]
Output: 99
解法一(較爲複雜)
大致思路
-
將輸入數組中的每個數看做是2進制數,由於待查找的數字(設爲
X
)只出現1次,而其他數字都出現了3次,因此對於這些2進制數而言,每個bit位上1出現的次數之和必定爲能夠整除3,或者餘數爲1(包括符號位),而餘數爲1的bit位,即代表待查找數X
上相同bit位上的值爲1,而待查找數上其他的bit位上爲0,因爲其他bit位上數值1出現次數必定爲3的整數倍(包括0),這表明代表此bit位上的1都來自於出現次數爲3的數字,而非待查找數字X
。 -
確定了大致思路之後,那麼接下來的工作就是統計輸入數組中的所有數各個bit位上1出現的次數之和,然後找出數值1出現次數之和除以3的餘數爲1的bit位,將對應bit位置爲1,其餘bit位置爲0,即可得待查找數
X
。
統計各bit位數值1出現次數:
-
假設
a,b
爲統計某個bit位1出現次數的2個計數位(因爲待計數範圍爲0~2,而2個計數位可以表示0~3,因此使用2個計數位),c
爲當前bit位下一個出現的值(0或1)。 -
設
a
爲高位,b
爲低位,當a=0,b=0
時,則表明當前bit位1出現的次數爲0;當a=0,b=1
時,則表明當前bit位1出現的次數爲1;當a=1,b=0
時,則表明當前bit爲1出現的次數爲2;當1出現3次時,則將計數位a和b置0,表明一個循環。 -
假設
a,b
計數器對應的bit位每次接收到待統計的值c
後,變化後的對應值爲a',b'
,則各種可能情況如下表所示:
計數器a,b代表值 | a | b | c | a’ | b’ | 計數器a’,b’代表值 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 1 | 1 | 1 | 0 | 2 |
2 | 1 | 0 | 1 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 0 | 1 | 1 |
2 | 1 | 0 | 0 | 1 | 0 | 2 |
小結:
由上表可知,從計數器從低位到高位的數值有以下規律(類似於進位,低位確定了才能確定高位):
b'
爲1時,只有兩種情況,即a=0, b=0,c=1
或a=0,b=1,c=0
時,兩種情況不會同時出現,且每個情況只會產生一個結果,故有表達式:
b' = ((~a) & (~b) & c) | ((~a) & b & (~c)) = (~a) &((~b) & c | b & (-c)) = (~a) & b^c
- 即
b' = (~a) & (b^c)
a'
爲1時,基於低位計數器b'
的值,只有兩種情況,即a=0, b'=0, c=1
或a=1, b'=0, c=0
時,兩種情況不會同時出現,且每個情況只會產生一個結果,故有表達式:
a' = ((~a) & (~b') & c) | (a & (~b') & (~c)) = (~b') & ((~a) & c | a & (~c)) = (~b') & a^c
- 即
a' = (~b') & (a^c)
PS:
上式最後的化簡,是根據邏輯代數的分配律以及異或運算等價式所得:
- 邏輯代數分配律:
a & (b|c) = a&b | a&c
- 異或運算等價式:
a^b = a&(~b) | (~a)&b
總結:
-
雖然上述所有的分析和操作都是針對於單個bit,但是由於每個操作都是按位運算,因此很容易就擴展到整個數的所有bit位上,故不論是10進制還是2進制數,此統計過程都適用。
-
最後對各個bit位上出現的次數求餘時,和之前類似,可以使用按位運算實現。當且僅當
a'=0,b'=1
時,對應bit位上1出現的次數爲1,即代表待查找數X
的此bit位上的數值也爲1,故設此bit位的值爲num
,則有num=(~a') & b'
,而在此題中,b'=1
只有一種情況,故直接有num = b'
代碼
-
時間複雜度:O(n)
-
空間複雜度:O(1)
class Solution {
public int singleNumber(int[] nums) {
// 計數器
int a = 0, b = 0;
// 遍歷原始數組,統計各個bit位上1出現的次數
for (int c : nums) {
b = (~a) & (b ^ c);
a = (~b) & (a ^ c);
}
// 最後根據計數器a,b獲取只出現1次1的bit位,最終按位與即爲只出現1次的數字
// return (~a) & b;
return b;
}
}
推廣
使用本文介紹的方法,同樣適用於求解形如此題的一些問題,只要待查找元素的出現次數小於其他元素,且其他元素出現次數相同,如:
給定一個非空整數數組,除了某個元素只出現2次以外,其餘每個元素均出現了5次。找出那個只出現了2次的元素。
計數器變化表:
計數器a,b,c代表值 | a | b | c | d | a’ | b’ | c’ | 計數器a’,b’,c’代表值 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 2 |
2 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 3 |
3 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 4 |
4 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
2 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 2 |
3 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 3 |
4 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 4 |
推導公式:
c'
= (~a)&(~b)&(~c)&d | (~a)&b&(~c)&d | (~a)&(~b)&c&(~d) | (~a)&b&c&(~d)
= (~a)&(~c)&d | (~a)&c&(~d)
= (~a)&(c^d)
b'
= (~a)&(~b)&(~c')&d | (~a)&b&c'&d | (~a)&b&(~c')&(~d) | (~a)&b&c'&(~d)
= (~a)&(~c')&(b^d) | (~a)&b&c
a'
= (~a)&(~b')&(~c')&d | a&(~b')&(~c')&(~d')
= (a^d)&(~b')&(~c')
num = (~a)&b&(~c);
代碼:
class Solution {
public int singleNumber(int[] nums) {
// 計數器
int a = 0, b = 0, c = 0;
// 遍歷原始數組,統計各個bit位上1出現的次數
for (int d : nums) {
c = (~a) & (c ^ d);
b = (~a) & (~c) & (b ^ d) | (~a) & b & c;
a = (a ^ d) & (~b) & (~c);
}
// 最後根據計數器a,b,c獲取出現2次1的bit位
return (~a) & b & (~c);
}
}
解法二(便於理解,較爲簡單)
主要思想
-
上述的繪製變化情況表,推導公式,化簡公式的求解方式,需要一次性窮舉所有可能的情況,這樣當數字出現次數較大時,光繪製變化情況表就要耗費大量的時間,更不用說還需要求解推導公式,對公式進行化簡,因此上述求解方式綜合來看並非是最優解法。
-
另一種思路依舊是採用計數器計數的方式,但與上述方法不同,而是讓計數器正常計數,當某個bit位上1出現次數達到循環點時(如在本題中,即爲3),則將對應bit位的所有計數器置爲0,重新開始計數,視爲新一輪循環。
計數器計數方式:
每次更新計數器時,從高位開始更新,當計數器低位全爲1,且當前bit位的下一個待統計值也爲1,則表明當前高位更新後的值需要加1。
假設a,b,c
爲計數器,依次從高到低,i
爲當前bit位的下一個待統計值,則當b=1,c=1,i=1
時,計數器a
的值需要加1;同理,當c=1,i=1
時,計數器b
的值需要加1;當i=1
時,計數器c
的值需要加1。由於各個計數器之間是相互獨立按位運算,需要避免進位的影響,需要使用不進位相加的方式進行按位運算(即異或運算),故有:
a = a^(b & c & i)
b = b^(c & i)
c = c^i
假設有一標記bit變量mark
,同於標記當前bit位上1出現的次數是否到達循環點(假設本題爲5),則當標誌位滿足a=1,b=0,c=1
時,則mark
等於0,表明需要將當前bit位上的計數器都置爲0,重新開始循環,其他情況下mark
爲1,故有:
mark = ~(a & (~b) & c)
當mark
爲1時,計數器正常計數,當mark
爲0時,則對應bit位表明到達循環點,需要將對應的計數器置爲0,每次求得mark
的最新值後,還要在最後還需要更新計數器:
a = a & mark
b = b & mark
c = c & mark
對於本題:
只需要兩個計數器a,b
,計數循環點爲3(a=1,b=1
),故有:
-
a = a^(b & i)
-
b = b^i
-
mark = ~(a & b)
-
a &= mark
-
b &= mark
將1出現次數爲1的bit位置爲1,則得到最終要查找的數:
num = (~a)&b
由於本題中待查找數出現次數爲奇數,因此只要將最低位計數器b
爲1時的對應的bit位置爲1,即可:
num = b
代碼
-
時間複雜度:O(n)
-
空間複雜度:O(1)
class Solution {
public int singleNumber(int[] nums) {
// 設置計數器,以及標記變量
int a = 0, b = 0, mark = 1;
// 遍歷原始數組,統計各個bit位上數值1出現的次數
for (int num : nums) {
a ^= b & num;
b ^= num;
// 計算標記變量mark,更新計數器的值,若達到循環點3(即a=1,b=1),則將計數器對應bit位置爲0,否則保持不變
mark = ~(a & b);
a &= mark;
b &= mark;
}
// 由於本題中,待查找數出現次數爲奇數次,因此只要每個bit位的最低計數位爲1,則待查找數對應bit位也爲1
return b;
}
}
推廣
使用本文介紹的方法,同樣適用於求解形如此題的一些問題,只要待查找元素的出現次數小於其他元素,且其他元素出現次數相同,如:
給定一個非空整數數組,除了某個元素只出現2次以外,其餘每個元素均出現了5次。找出那個只出現了2次的元素。
class Solution {
public int singleNumber(int[] nums) {
// 設置計數器,以及標記變量
int a = 0, b = 0, c = 0, mark = 1;
// 遍歷原始數組,統計各個bit位上數值1出現的次數
for (int i : nums) {
a ^= b & c & i;
b ^= c & i;
c ^= i;
// 計算標記變量mark,並更新計數器的值,若達到循環點5(即a=1,b=0,c=1)
// 則將計數器對應bit位置爲0,否則保持不變
mark = ~(a & (~b) & c);
a &= mark;
b &= mark;
c &= mark;
}
// 當最終計數器a=0,b=1,c=0時,則表明待查找數對應bit位值爲1
return (~a) & b & (~c);
}
}
參考資料
LeetCode 137. Single Number II-Discuss
Leetcode 137:只出現一次的數字 II(最詳細的解法!!!)