1. 程式人生 > >LeetCode137——只出現一次的數字II

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

我的LeetCode程式碼倉:https://github.com/617076674/LeetCode

原題連結:https://leetcode-cn.com/problems/single-number-ii/description/

題目描述:

知識點:位運算

思路:位運算

本題是LeetCode136——只出現一次的數字的加強版,下述解法討論均參考於Detailed explanation and generalization of the bitwise operation method for single numbers

(1)問題的一般化:

給定一個非空整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現了k(k > 1)次。找出那個只出現了p(p >= 1且p % k != 0)次的元素。

(2)考慮陣列中的元素不是0就是1的情況,即1位的情況

整數在計算機中是按位儲存的,一個int型的整數在計算機中是按32位儲存的。我們先考慮陣列中的元素不是0就是1的情況,即1位的情況。現在,我們需要統計陣列中1的數量,我們的統計規則是:當1的數量達到k時,從0開始重新計數,即1的數量為k時,相當於1的數量為0,而1的數量為k + 1時,相當於1的數量為1。我們用一個m位的二進位制數:xm, ..., x1來對1的數量進行計數。

從上述分析中,我們可以得出以下4點結論:

a:計數器的初始值是0,即xi = 0(i = 1, ..., m)

b:遍歷陣列中的每一個元素,一旦遇到0,我們的計數器保持不變

c:遍歷陣列中的每一個元素,一旦遇到1,我們的計數器需要加1

d:對於一個m位的二進位制數,其能計數到的最大值是2 ^ m - 1,而我們現在的計數最大值需要能夠達到k - 1,因此需要滿足條件2 ^ m >= k

現在的關鍵問題在於:當我們遍歷陣列時,我們採取什麼樣的位操作使得xm, ..., x1的值發生變化呢?

考慮上述結論b,我們可以使用x = x | 0x = x ^ 0這兩種操作來使得我們的計數器在遇到0時保持不變,我們究竟應該選擇哪種操作呢?

對實際過程進行模擬,從初始時刻開始,初始時xi = 0(i = 1, ..., m)。由於我們之前已經考慮了結論b,我們來考慮結論c。在遍歷陣列中的元素時,一旦我們遇到了第一個1,我們需要改變計數器的狀態為:xm = 0, ..., x2 = 0, x1 = 1

。當我們遇到第二個1時,我們需要改變計數器的狀態為:xm = 0, ..., x2 = 1, x1 = 0。對於x1 = x1 | nums[i]操作,在遇到第二個1時,顯然無法將x1從1變為0,而x1 = x1 ^ nums[i]卻可以。因此,我們選擇的位操作是x1 = x1 ^ nums[i]

現在,我們的問題變成了:xm, ..., x2, x1狀態發生改變的條件是什麼?

對於x1,其發生改變的條件是:遍歷陣列時,遇到1

對於x2,其發生改變的條件是:遍歷陣列時,遇到1,且x1 = 1。因為如果此時x1 = 0,那麼我們只需令x1 = 1就可以達到計數器加1的目的,無需改變其他位的值。

同理,對於xm,其發生改變的條件是,遍歷陣列時,遇到1,且xm - 1, ..., x1均為1

現在,我們還存在的問題是:對於m位的二進位制數xm, ..., x1,其計數歸0的值是2 ^ m,而不是k。如何使得計數器在達到k時歸0?

我們需要設定一個變數mask,當計數器的值到達k時,mask = 0,其餘時刻mask = 1。這樣定義之後,我們只需要令xm = xm & mask, ..., x1 = x1 & mask就可以實現計數器在達到k時歸0的目的。

那麼,我們的問題就變成了:如何計算mask的值,使得其滿足上述定義呢?

對於數字k,我們將其表示成二進位制形式km, ..., k1。當我們的計數達到k時,顯然有xi = ki(i = 1, ..., m)。因此,mask = ~(y1 & y2 & ...& ym),如果kj = 1,那麼yj = xj;如果kj = 0,那麼yj = ~xj(j = 1, ..., m)

由上述分析可知,我們的演算法的偽碼如下:

for (int i : nums) {
    xm ^= (xm-1 & ... & x1 & i);
    xm-1 ^= (xm-2 & ... & x1 & i);
    .....
    x1 ^= i;
    
    mask = ~(y1 & y2 & ... & ym) where yj = xj if kj = 1, and yj = ~xj if kj = 0 (j = 1 to m).

    xm &= mask;
    ......
    x1 &= mask;
}

(3)考慮陣列中的元素是32位數的情況

現在,我們需要將陣列中的元素是1位數的情況推廣到32位數的情況。一個想法是對每一位都建立一個計數器,這樣就總共有32個計數器。當然,利用位運算的性質,我們可以用m個32位的整數來替代32個m位計數器,理由很簡單:按位運算僅適用於每個位,因此對不同位的操作彼此獨立。示意圖如下:

頂部的長方框代表一個32位的整數,每個位都對應一個m位的計數器(顯示在相應方框的下面)。由於按位運算對不同位的操作是彼此獨立的,我們可以把所有計數器的第m位組成一個32位的整數(如橙色方框所示)。在這個32位數中的所有位有著相同的位運算操作。由於每個計數器有m位,我們得到m個32位整數xm, ..., x1,但是現在這些都是32位的數,而不是(2)中的1位數,其餘的演算法部分和(2)中均相同。

(3)返回什麼結果

最後,我們需要明確的一個問題是:在xm, ..., x1中,哪一個是我們尋找的那個數呢?即哪個是出現了p次的數呢?

為了回答這個問題,我們需要理解xm, ..., x1分別代表的含義。以x1為例,其有32位,我們將其標記為r(r = 1, ..., 32)。在我們掃描了整個陣列之後,x1的第r位由陣列中所有數字的第r位確定。更準確地說,假設陣列中所有元素的第r位的1的數量為q,令q' = q % k,將q'表示成二進位制形式:q'm, ..., q'1,根據定義,x1的第r位和q'1相等

那麼,現在我們的問題是:如果x1的第r位是1,這代表著什麼呢?

回答這個問題的關鍵是,我們必須明確哪些值能對x1的第r位造成影響?

那些在陣列中出現了k次的的數字顯然不會對x1的第r位造成影響。對x1的第r位造成影響的元素必須滿足兩個條件:

a:該元素的第r位是1

b:該元素在陣列中的出現次數不是k的整數倍

條件a是顯而易見的。對於條件b,是因為當該元素的出現次數滿k次時會歸0。因此,只有那個出現了p(p % k != 0)次的元素會對x1的第r位造成影響。如果p > k,那麼前面的k * [p / k]([p / k]代表p / k的整數部分)個元素不會對x1的第r位造成影響。因此,我們需要令p' = p % k,且該元素出現的有效次數其實是p'次。

將p'寫成二進位制形式:p'm, ..., p'1(由於p' < k,所有其可以填充進m位二進位制數裡)。我們先丟擲一個結論:

xj和那個出現了p次的元素相等的條件是p'j = 1(j = 1, ..., m),下面給出證明。

如果xj的第r位是1,證明那個出現了p次的元素的第r位也是1(否則,沒有任何因素能使得xj的第r位是1)。我們只需要證明:如果xj的第r位是0,那麼那個出現了p次的元素的第r位也一定是0

當xj的第r位是0時,假設那個出現了p次的元素的第r位是1。在遍歷完整個陣列之後,這個1會被計數p'次。根據定義,xj的第r位和p'j相等,而p'j = 1,因此xj的第r位時1,這就產生了矛盾。因此,我們得出結論:當p'j = 1時,xj的第r位和那個出現了p次的元素的第r位相等。由於對r = 1, ..., 32的每一位都有上述結論,因此當p'j = 1時,有xj和那個出現了p次的元素相等

所以我們可以返回任意一個xj,只要滿足p'j = 1即可

演算法的時間複雜度是O(n * logk),其中n為陣列中的元素個數。空間複雜度是O(logk)。

注:

事實上有如下關係:(xj)_r = s_r & p'j,其中(xj)_r代表xj的第r位,s_r代表那個出現了p次的元素的第r位。

當p'j = 1時,(xj)_r = s_r。

當p'j = 0時,(xj)_r = 0。

因此我們得到:

當p'j = 1時,xj = s。

當p'j = 0時,xj = 0。

因此,我們最後也可以返回(x1 | x2 | ... | xm)

對於本題而言,k = 3(二進位制:11),p = 1(二進位制:1)。因此我們只需取m = 2,用兩個32位數字x2和x1作為計數器。由於2 ^ m = 4 > k,因此我們需要一個mask變數,其值取為~(x1 & x2)。由於p表示為二進位制是1,因此我們直接返回x1即可。當然,我們也可以選擇返回(x1 | x2)。

時間複雜度是O(n)。空間複雜度是O(1)。

JAVA程式碼:

public class Solution {
    public int singleNumber(int[] nums) {
        int x1 = 0, x2 = 0, mask = 0;
        for (int i = 0; i < nums.length; i++) {
            x2 ^= x1 & nums[i];
            x1 ^= nums[i];
            mask = ~(x1 & x2);
            x2 &= mask;
            x1 &= mask;
        }
        return x1;
    }
}

LeetCode解題報告: