劍指 Offer 56 - I. 數組中數字出現的次數
一個整型數組nums 里除兩個數字之外,其他數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。要求時間復雜度是O(n),空間復雜度是O(1)。
示例 1:
輸入:nums = [4,1,4,6]
輸出:[1,6] 或 [6,1]
示例 2:
輸入:nums = [1,2,10,4,1,4,3,3]
輸出:[2,10] 或 [10,2]
限制:
- 2 <= nums.length <= 10000
做題思路:
這道題思路是力友那我總不能天天CV吧寫的可以讓我們這種菜雞好好學習一下。
異或滿足交換律,第一步異或,相同的數其實都抵消了,剩下兩個不同的數。這兩個數異或結果肯定有某一位為1,不然都是0的話就是相同數。找到這個位,不同的兩個數一個在此位為0,另一個為1。按此位將所有數分成兩組,分開后各自異或,相同的兩個數異或肯定為0(而且分開的時候,兩個數必為一組)。剩下的每組里就是我門要找的數。
難點主要在於對m的理解。m是一個二進制數,且其中只有一位是1,其他位全是0,比如000010,表示我們用倒數第二位作為分組標准,倒數第二位是0的數字分到一組,倒數第二位是1的分到另一組。
舉個例子:z = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位為1.我們將m初始化為1,如果(z & m)的結果等於0說明z的最低為是0我們每次將m左移一位然后跟z做與操作,直到結果不為0.此時m應該等於1000,同z一樣,第四位為1.
代碼和詳細思路如下:
class Solution {
public int[] singleNumbers(int[] nums) {
//因為相同的數字異或為0,任何數字與0異或結果是其本身。
//所以遍歷異或整個數組最后得到的結果就是兩個只出現一次的數字異或的結果:即 z = x ^ y
int z = 0;
for(int i : nums) z ^= i;
//我們根據異或的性質可以知道:z中至少有一位是1,否則x與y就是相等的。
//我們通過一個輔助變量m來保存z中哪一位為1.(可能有多個位都為1,我們找到最低位的1即可)。
//舉個例子:z = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位為1.
//我們將m初始化為1,如果(z & m)的結果等於0說明z的最低為是0
//我們每次將m左移一位然后跟z做與操作,直到結果不為0.
//此時m應該等於1000,同z一樣,第四位為1.
int m = 1;
while((z & m) == 0) m <<= 1;
//我們遍歷數組,將每個數跟m進行與操作,結果為0的作為一組,結果不為0的作為一組
//例如對於數組:[1,2,10,4,1,4,3,3],我們把每個數字跟1000做與操作,可以分為下面兩組:
//nums1存放結果為0的: [1, 2, 4, 1, 4, 3, 3]
//nums2存放結果不為0的: [10] (碰巧nums2中只有一個10,如果原數組中的數字再大一些就不會這樣了)
//此時我們發現問題已經退化為數組中有一個數字只出現了一次
//分別對nums1和nums2遍歷異或就能得到我們預期的x和y
int x = 0, y = 0;
for(int i : nums) {
//這里我們是通過if...else將nums分為了兩組,一邊遍歷一遍異或。
//跟我們創建倆數組nums1和nums2原理是一樣的。
if((i & m) == 0) x ^= i;
else y ^= i;
}
return new int[]{x, y};
}
}
這個是K神更為簡潔的代碼如下:
class Solution {
public int[] singleNumbers(int[] nums) {
int x = 0, y = 0, n = 0, m = 1;
for(int num : nums) // 1. 遍歷異或
n ^= num;
while((n & m) == 0) // 2. 循環左移,計算 m
m <<= 1;
for(int num: nums) { // 3. 遍歷 nums 分組
if((num & m) != 0) x ^= num; // 4. 當 num & m != 0
else y ^= num; // 4. 當 num & m == 0
}
return new int[] {x, y}; // 5. 返回出現一次的數字
}
}
