原始题目:剑指 Offer 56 - I. 数组中数字出现的次数 (opens new window)

解题思路:

异或的定义:如果两个值相同,异或结果为 00,否则为 11

异或有两个重要性质

aa=0a0=aa ⊕ a = 0 \\ a ⊕ 0 = a \\

因此可以有

aabc...bcx=000x=x\begin{align} &a ⊕ a ⊕ b ⊕ c ⊕ ... ⊕ b ⊕c ⊕x \\ &= 0 ⊕ 0 ⊕ 0 ⊕ x \\ &= x \end{align}

回到题目,将数组中相同的数字进行异或,则可以消除掉所有出现两次的数字,最后会只剩下两个只出现一次的数字 xxyy 的异或结果 xorxor

xorxor 的二进制中为 1 的地方,就是 xxyy 不同的地方。可以找到 xorxor 中第一个为 11 的地方,假设为 bibi

那么可以将整个数组分为两部分,一部分是 bibi11 的数字,一部分是 bibi00 的数字。而且这两个只出现一次的数字会分别划分到这两个部分中。

如何快速找到 xorxor 的二进制中第一个为 11 的地方(从右到左)?

通过 nn & ((~n+1)n + 1) 这个运算过程,可以得到 nn 中第一个 11 的位置。

假设 n=101010n = 101010,则 nn 取反后为 010101010101。此时 nn & ~n=0n = 0n+1=010110~n + 1 = 010110 ,则 nn & ((~n+1)=000010n + 1) = 000010

代码:

public int[] singleNumbers(int[] nums) {
    if (nums == null || nums.length == 0 || nums.length % 2 == 1) {
        return new int[0];
    }
    int xor = 0;
    for (int num : nums) {
        xor ^= num;
    }
    int m = xor & (~xor + 1);
    int x = 0, y = 0;
    for (int num : nums) {
        // 分组异或
        if ((num & m) != 0) {
            x ^= num;
        } else {
            y ^= num;
        }
    }
    // 其中没有两个不同的数字
    if (x == 0 && x == y) {
        return new int[0];
    }
    return new int[]{x, y};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

复杂度分析

  • 时间复杂度O(N)O(N):线性遍历 numsnums 使用 O(N)O(N) 时间。
  • 空间复杂度O(1)O(1):辅助变量占用常数大小的额外空间。
上次更新: 2023/10/15