40、数组中只出现一次的两个数字

方法一:

题目给的意思分析之后,很容易想到一种方法,就是用哈希表辅助得到这两个只出现一次的数字。

代码思路:

1、创建一个哈希表

2、当数组元素没有在哈希表中成为key的时候,put进哈希表,当已存在的时候,则remove掉。

3、最后哈希表中剩下的key就是只出现一次的数字

4、遍历key然后返回结果

直接贴出代码:

public int[] FindNumsAppearOnce (int[] array) {
        // write code here
        // 用于返回结果
        int[] res = new int[2];
        // 创建一个哈希表
        HashMap<Integer,Object> set = new HashMap<>();
        for(int i = 0; i < array.length; i++){
            // 如果已经被当作key了,那就直接remove掉
            if(set.containsKey(array[i])){
                set.remove(array[i],null);
            }else{
                // 否则就添加进去
                set.put(array[i],null);
            }
        }
        int i = 0;
        // 最后拿出来放进返回结果的数组中进行返回
        for(Integer num:set.keySet()){
            res[i++] = num;
        }
        return res;
    }

这个方法非常常规,容易想到并且也容易实现。但是它的时间复杂度和空间复杂度都是O(N)。

那么,如果给这道题目加上一个限制(用O(1)的空间复杂度实现),也就是说不能用哈希表做了,我们还能想到其它的思路吗?

所以,下面我们就用空间复杂度为O(1)的做法来解决这道题目吧~

方法二:位运算

对于这道题目,我们先来想另外一个问题:

如果数组中只有一个出现了一次的数字,我们想到得到它,那么应该如何解决呢?

我们都知道异或运算:如果两个数一样则异或结果为0,不一样则异或结果为1。(二进制)

(0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0)

举个例子:

4 ⊕ 4 = 0,将4化为二进制为 0100

所以 0100

异或 0100

得到 0000

4 ⊕ 4 ⊕ 5 = 5

则 0100

​ 0100

​ 0101

得到 0101

我们可以看到上面的运算过程,因为4=4,两者相等异或结果为0。所以0异或任意数都等于任意数。

所以,当只有一个出现了一次的数字的时候,则只需要将全部数进行异或运算,运算结果就剩下了那个只出现一次的数字了。

public int[] singleNumber(int[] nums) {
    int x = 0;
    for(int num : nums)  // 1. 遍历 nums 执行异或运算
        x ^= num;
    return x;            // 2. 返回出现一次的数字 x
}

好了,上面说了这么多,那这道题目是找两个只出现一次的数字呀~

上面的方法又是针对只出现一次的数字,假设我也一样全部执行异或运算 1⊕4⊕1⊕6,最后也还是会剩下4⊕6呀~

我们看看:

0100 ⊕ 0110 = 0010 这个结果也不能得出什么东西哇~

我们换个角度思考,能不能做个分组,将题目分为两组 ,然后每一组求出其中的出现一次的数字,最后两者一起返回,不就解决问题了吗?

那么我们要如何分组呢?位运算进行分组,我们首先想到的应该是奇偶分组,就是将所有数 &1,此时能将数字分为奇偶两组。

但是这个时候问题又来了,你又不能保证两个数字就一奇一偶,有可能都是奇数也有可能都是偶数呀~

但是,我们想一下,&1的操作,归根到底,是按照二进制最低位的不同来分组的,

例如 : 0011(3) ,0101(5),0100(4),0001(1)

对上面四个数分组,我们都&1,可以分得结果: 0011,0101,0001(奇数) 0100 (偶数)

我们很明显能够知道,当二进制&1结果为1的时候,为奇数,反之为偶数。它们是按照最低位的不同来分组的。

上面我们知道,能够将数字分为 奇偶两组,那么现在,我再给出一个难度,如何区分出 0011,0101 ?

对 0011,0101 这两个数进行分组,我们可以观察到最低位都为1,此时如果我们还是进行&1操作去分组,那肯定是分不出来的!

因为两数的最低为都是一样的,&1之后还是1,还是无法区分,那么我们看到最低的第二位0011是1,0101是0,很明显这两位就不一样,那么我们就可以将这两数&0010呀,不就能够区分出来了吗?

0011 &0010 = 0010 0101&0010 = 0000,此时还是根据结果是否为0得到分组!

那要是是 0100 和 1100呢?如何分组呢? 不就是&1000 就能够分组了吗?

所以,说了那么多,其实就是为了推出一个分组的方式,两个不同的数如何分组!

我们都知道两个不同的数,那么它的二进制表示肯定是不一样的!这是毋庸置疑的!

所以,我们要想对两者进行分组操作,就是需要找到两者中的那一位不同的二进制,然后得到分组的与值(去&的那个值),问题不就解决了吗?

那要怎么找到那一位不同的二进制呢?

我们看一个例子: 1,1,4,6

全部做异或运算结果为 4⊕6 = 0100⊕0110 = 0010

异或的运算规则是什么? 相同的为0,不同的为1。所以我们根据两者异或出来的结果 0010,不就可以知道那一位不同了嘛?(为1的那一位就是不同的)

好了,说了这么多,下面安排代码把~

public int[] FindNumsAppearOnce (int[] array) {

        // 先将全部数进行异或运算,得出最终结果
        int tmp = 0;
        for(int num: array){
            tmp ^= num;
        }

        // 找到那个可以充当分组去进行与运算的数
        // 从最低位开始找起
        int mask = 1;
        while((tmp&mask) == 0){
            mask <<= 1;
        }

        // 进行分组,分成两组,转换为两组 求出现一次的数字 去求
        int a = 0;
        int b = 0;
        for(int num:array){
            if((num&mask) == 0){
                a ^= num;
            }else{
                b ^= num;
            }
        }
        // 因为题目要求小的数放前面,所以这一做个判断
        if(a > b){
            int c = a;
            a = b;
            b = c;
        }
        return new int[]{a,b};
    }

复杂度分析:

时间复杂度:O(N)。数组的长度n,循环。

空间复杂度:O(1)。几个变量的空间。