题目描述(中等难度)

所有数字都出现了两次,只有两个数字都只出现了 1 次,找出这两个数字。

解法一

最直接的方法,统计每个数出现的次数。使用 HashMap 或者 HashSet,由于每个数字最多出现两次,我们可以使用 HashSet

遍历数组,遇到的数如果 HashSet 中存在,就把这个数删除。如果不存在,就把它加入到 HashSet 中。最后 HashSet 中剩下的两个数就是我们要找的了。

public int[] singleNumber(int[] nums) {
    HashSet<Integer> set = new HashSet<>();
    for (int n : nums) {
        if (set.contains(n)) {
            set.remove(n);
        } else {
            set.add(n);
        }
    }
    int[] result = new int[2];
    int i = 0;
    for (int n : set) {
        result[i] = n;
        i++;
    }
    return result;
}

解法二

我们之前做过 136 题 ,当时是所有数字都是成对出现的,只有一个数字是落单的,找出这个落单的数字。其中介绍了异或的方法,把之前的介绍先粘贴过来。

还记得位操作中的异或吗?计算规则如下。

0 ⊕ 0 = 0

1 ⊕ 1 = 0

0 ⊕ 1 = 1

1 ⊕ 0 = 1

总结起来就是相同为零,不同为一。

根据上边的规则,可以推导出一些性质

  • 0 ⊕ a = a
  • a ⊕ a = 0

此外异或满足交换律以及结合律。

所以对于之前的例子 a b a b c c d ,如果我们把给定的数字相互异或会发生什么呢?

  a ⊕ b ⊕ a ⊕ b ⊕ c ⊕ c ⊕ d
= ( a ⊕ a ) ⊕ ( b ⊕ b ) ⊕ ( c ⊕ c ) ⊕ d
= 000 ⊕ d
= d

然后我们就找出了只出现了一次的数字。

这道题的话,因为要寻找的是两个数字,全部异或后不是我们所要的结果。介绍一下 这里-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的思路。

如果我们把原数组分成两组,只出现过一次的两个数字分别在两组里边,那么问题就转换成之前的老问题了,只需要这两组里的数字各自异或,答案就出来了。

那么通过什么把数组分成两组呢?

放眼到二进制,我们要找的这两个数字是不同的,所以它俩至少有一位是不同的,所以我们可以根据这一位,把数组分成这一位都是 1 的一类和这一位都是 0 的一类,这样就把这两个数分到两组里了。

那么怎么知道那两个数字哪一位不同呢?

回到我们异或的结果,如果把数组中的所有数字异或,最后异或的结果,其实就是我们要找的两个数字的异或。而异或结果如果某一位是 1,也就意味着当前位两个数字一个是 1 ,一个是 0,也就找到了不同的一位。

思路就是上边的了,然后再考虑代码怎么写。

怎么把数字分类?

我们构造一个数,把我们要找的那两个数字二进制不同的那一位写成 1,其它位都写 0,也就是 0...0100...000 的形式。

然后把构造出来的数和数组中的数字相与,如果结果是 0,那就意味着这个数属于当前位为 0 的一类。否则的话,就意味着这个数属于当前位为 1 的一类。

怎么构造 0...0100...000 这样的数。

由于我们异或得到的数可能不只一位是 1,可能是这样的 0100110,那么怎么只留一位是 1 呢?

方法有很多了。

比如,201 题 解法三介绍的 Integer.highestOneBit 方法,它可以保留某个数的最高位的 1,其它位全部置 0,源码的话当时也介绍了,可以过去看一下。

最后,总结下我们的算法,我们通过要找的两个数字的某一位不同,将原数组分成两组,然后组内分别进行异或,最后要找的数字就是两组分别异或的结果。

然后举个具体的例子,来理解一下算法。

[1,2,1,3,2,5]

1 = 001
2 = 010
1 = 001
3 = 011
2 = 010
5 = 101

把上边所有的数字异或,最后得到的结果就是 3 ^ 5 = 6 (110) 

然后对 110 调用 Integer.highestOneBit 方法就得到 100, 我们通过倒数第三位将原数组分类

倒数第三位为 0 的组
1 = 001
2 = 010
1 = 001
3 = 011
2 = 010

倒数第三位为 1 的组    
5 = 101

最后组内数字依次异或即可。

再结合代码,理解一下。

public int[] singleNumber(int[] nums) {
    int diff = 0;
    for (int n : nums) {
        diff ^= n;
    }
    diff = Integer.highestOneBit(diff);
    int[] result = { 0, 0 };
    for (int n : nums) {
        //当前位是 0 的组, 然后组内异或
        if ((diff & n) == 0) {
            result[0] ^= n;
        //当前位是 1 的组
        } else {
            result[1] ^= n;
        }
    }
    return result;
}

这里-time-O(1)-space-7-line-Solution-with-Detail-Explanation) 提出了一个小小的改进。

假如我们要找的数字是 ab,一开始我们得到 diff = a ^ b。然后通过异或我们分别求出了 ab

其实如果我们知道了 ab 的话可以通过一次异或就能得到,b = diff ^ a

public int[] singleNumber(int[] nums) {
    int diff = 0;
    for (int n : nums) {
        diff ^= n;
    }
    int diff2 = Integer.highestOneBit(diff);
    int[] result = { 0, 0 };
    for (int n : nums) {
        //当前位是 0 的组, 然后组内异或
        if ((diff2 & n) == 0) {
            result[0] ^= n;
        } 
    }
    result[1] = diff ^ result[0];
    return result;
}

得到只有一位 1 的数,除了 Integer.highestOneBit 的方法还有其他的做法。

这里-time-O(1)-space-Easy-Solution-with-Detail-Explanations) 的做法。

 diff &= -diff;

取负号其实就是先取反,再加 1,需要 补码 的知识。最后再和原数相与就会保留最低位的 1。比如 1010,先取反是 0101,再加 1,就是 0110,再和 1010 相与,就是 0010 了。

还有 这里-time-and-O(1)-space-easy-understaning-with-simple-explanation) 的做法。

 diff = (diff & (diff - 1)) ^ diff;

n & (n - 1) 的操作我们在 191 题 用过,它可以将最低位的 1 置为 0。比如 1110,先将最低位的 1 置为 0 就变成 1100,然后再和原数 1110 异或,就得到了 0010

还有 这里 的做法。

diff = xor & ~(diff - 1);

先减 1,再取反,再相与。比如 10101 就是 1001,然后取反 0110,然后和原数 1010 相与,就是 0010 了。

还有 这里 的做法。

int mask=1;
while((diff & mask)==0)
{
    mask<<=1;
}
//mask 就是我们要构造的了

这个方法比较直接,依次判断哪一位是 1

解法一的话经常用了,最容易想到的方法。

解法二的话,将问题转换成基本问题,这个思想经常用到,但有时候也比较难想。后边总结的得到只包含一个 1 的二进制的各种骚操作比较有意思。

results matching ""

    No results matching ""