LeetCode 137.Single Number II(只出現一次的數 II)

題目

LeetCode: 137. Single Number II

力扣: 137. 只出現一次的數字 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=1a=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=1a=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

LeetCode137——只出現一次的數字II

Leetcode 137:只出現一次的數字 II(最詳細的解法!!!)


End~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章