找到陣列中出現特定次數數字的問題

Grey Zeng發表於2021-10-09

找到陣列中出現特定次數數字的問題

問題一:一個陣列中有一種數出現了奇數次,其他數都出現了偶數次,怎麼找到並列印這種數

牛客-NowCoder_EvenOddTimes

LeetCode_0136_SingleNumber

問題一解題思路

因為a ^ a = 0, 所以出現過偶次的數異或結果都是0,又因為0^a=a,所以把陣列中所有的數做異或以後的結果,就是出現了奇數次的那個數。

問題一完整程式碼

public class LeetCode_0136_SingleNumber {

    public static int singleNumber(int[] nums) {
        int ans = nums[0];
        for (int i = 1; i < nums.length; i++) {
            ans ^= nums[i];
        }
        return ans;
    }
}

問題二:一個陣列中有兩種數出現了奇數次,其他數都出現了偶數次,怎麼找到並列印這兩種數

LeetCode_0260_SingleNumberIII

問題二解題思路

根據問題一的結論,假設陣列中ab這兩個數出現了奇數次,這個陣列中的所有數字異或以後得到的結果一定a^b

因為ab是兩種不同的數,所以a^b的結果一定不等於0。

所以,a^b的結果如果轉換成二進位制的話,一定有某位是1。我們假設a^b轉換成二進位制後最右側位置的1在i位置,由此可以得出一個結論:a和b的二進位制在i位置一定一個為0,一個為1

不妨假設ai位置為0bi位置為1

此外,容易得知,整個陣列中的數,i位置為0的數除了a以外,其他數一定有偶數個, i位置為1的數除了b之外,其他數一定有偶數個。

那麼我們可以只對i位置為1的數求異或,最後得到的值一定是b,然後通過b^(a^b) = a,可以得到a的值。

最後只剩下一個問題:

如何求一個數最右側的1呢?

假設 某個數x二進位制為:00010010, 其最右側的1是:00000010

演算法是:對於一個數x來說,它最右側的1等於x & ((~x) + 1)或者x & (-x)

所以,如果一個數是a^b,那麼它最右側的1就是(a^b) & (~(a^b) + 1)

我們用(a^b) & (~(a^b) + 1)這個值去和陣列中每個值數做與運算(&),如果與完以後的結果是0,說明這個數i位置是0,否則說明這個數i位置是1。我們前面已經得到一個結論,i位置為0的數除了a以外,其他數一定有偶數個。所以,用(a^b) & (~(a^b) + 1)這個值和每個i位置是0的陣列元素做與運算以後,最後的結果一定是a。 得到a以後,然後通過a^(a^b) = b,可以得到b的值。

問題二完整程式碼

public class LeetCode_0260_SingleNumberIII {
    public static int[] singleNumber(int[] arr) {
        int eor = 0;
        for (int n : arr) {
            eor ^= n;
        }
        // 假設出現奇數次的兩種數為 a和b
        // eor = a ^ b
        // 獲取最右側的1
        int a = 0;
        int rightOne = eor & ((~eor) + 1);
        for (int n : arr) {
            if ((n & rightOne) == 0) {
                a ^= n;
            }
        }
        int b = a ^ eor;
        return new int[]{a, b};
    }
}

當有如下公式計算一個數最右側的1以後

x & ((~x) + 1)

我們還可以解決如下問題:

LeetCode_0191_NumberOfOneBits

思路:即提取出最右側的1以後,與目標數進行與運算(&), 得到一個新的目標數,然後繼續提取新目標數的最右側的1,如此往復,即可把所有位置的1都提取出來。

問題三 一個陣列中有一種數出現k次,其他數都出現了m次,m > 1, k < m, 找到出現了k次的數

要求:假設陣列中所有數都是int型別,額外空間複雜度O(1),時間複雜度O(N)

LeetCode_0137_SingleNumberII

問題三解題思路

我們可以這樣考慮,設定一個32位的陣列,

int[] help = new int[32];

遍歷原始陣列中每個數num的每一個二進位制位, 虛擬碼如下:

for (int num : arr) {
  for (int i = 0; i < 32; i++) {
    help[i] += num的二進位制中i位置的值(只能是0或者1)
  }
}

經過以上迴圈,help陣列就把陣列中的所有數的二進位制位上的資訊累加起來了。

help[0]表示陣列中所有數二進位制中0位置的值之和;

help[1]表示陣列中所有數二進位制中1位置的值之和;

......

help[31]表示陣列中所有數二進位制中31位置的值之和。

然後i0位置開始拿出help[i]的值,假設help[i]=x,用x % m, 如果結果是k,說明出現k次的元素在這個位置上是1, 否則,這個出現了k次的數在i位置上是0, 遍歷完help陣列,出現k次元素的每一位資訊都拿到了,然後還原出來即可。

問題三完整程式碼

public static int km(int[] arr, int k, int m) {
  int[] helper = new int[32];
  for (int i = 0; i < arr.length; i++) {
   for (int j = 0; j < 32; j++) {
    helper[j] += ((arr[i] >> j) & 1);
   }
  }
  int ans = 0;
  for (int i = 0; i < 32; i++) {
   if (helper[i] % m == k) {
    ans |= (1 << i);
   }
  }
  return ans;
 }

更多

演算法和資料結構筆記

參考資料

相關文章