数组中只出现一次的数字

题目

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

思路

大家首先想到的是顺序扫描法,但是这种方法的时间复杂度是O(n^2)。接着大家又会考虑用哈希表的方法,但是空间复杂度不是O(1)。

应该怎么做才能即满足时间复杂度是O(n)又满足空间复杂度是O(1)的要求呢?

我们可以想一想“异或”运算的一个性质,我们直接举例说明。

举例:{2,4,3,6,3,2,5,5}

这个数组中只出现一次的两个数分别是4和6。怎么找到这个两个数字呢?

我们先不看找到俩个的情况,先看这样一个问题,如何在一个数组中找到一个只出现一次的数字呢?比如数组:{4,5,5},唯一一个只出现一次的数字是4。

我们知道异或的一个性质是:任何一个数字异或它自己都等于0。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字。比如数组{4,5,5},我们先用数组中的第一个元素4(二进制形式:0100)和数组中的第二个元素5(二进制形式:0101)进行异或操作,0100和0101异或得到0001,用这个得到的元素与数组中的三个元素5(二进制形式:0101)进行异或操作,0001和0101异或得到0100,正好是结果数字4。这是因为数组中相同的元素异或是为0的,因此就只剩下那个不成对的孤苦伶仃元素。

现在好了,我们已经知道了如何找到一个数组中找到一个只出现一次的数字,那么我们如何在一个数组中找到两个只出现一次的数字呢?如果,我们可以将原始数组分成两个子数组,使得每个子数组包含一个只出现一次的数字,而其他数字都成对出现。这样,我们就可以用上述方法找到那个孤苦伶仃的元素。

我们还是从头到尾一次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数组的异或结果。因为其他数字都出现了两次,在异或中全部抵消了。由于两个数字肯定不一样,那么异或的结果肯定不为0,也就是说这个结果数组的二进制表示至少有一个位为1。我们在结果数组中找到第一个为1的位的位置,记为第n位。现在我们以第n位是不是1为标准把元数组中的数字分成两个子数组,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。

举例:{2,4,3,6,3,2,5,5}

我们依次对数组中的每个数字做异或运行之后,得到的结果用二进制表示是0010。异或得到结果中的倒数第二位是1,于是我们根据数字的倒数第二位是不是1分为两个子数组。第一个子数组{2,3,6,3,2}中所有数字的倒数第二位都是1,而第二个子数组{4,5,5}中所有数字的倒数第二位都是0。接下来只要分别两个子数组求异或,就能找到第一个子数组中只出现一次的数字是6,而第二个子数组中只出现一次的数字是4。

代码

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        if(array == null || array.length ==0){
            return;
        }
        int temp = array[0];
        for(int i =1;i<array.length;i++){
            temp = temp ^ array[i];
        }
        int index = findLoc(temp);
        for(int i =0;i< array.length;i++){
            if(helper(array[i],index)){
                num1[0] = num1[0]^array[i];
            }else{
                num2[0] = num2[0]^array[i];   
            }
        }
    }
    public boolean helper(int num,int index){
        num >>>= index;
        if((num & 1)==1){
            return true;
        }
        return false;
    }
    public int findLoc(int nums){
        int index =0;
        while((nums &1)==0 && index <32){
            nums >>>=1;
            index ++;
        }
        return index;
    }
}

 

public class Test3 {
    public static void main(String[] args) {
        int[] nums ={1,2,1,3,2,5};
        System.out.println(singleNumber(nums));
    }
    public static int[] singleNumber(int[] nums) {
        int[] res ={0,0};
        if(nums == null || nums.length < 1){
            return res;
        }

        int result = nums[0];
        for (int i = 1; i < nums.length; i++) {
            result = result ^ nums[i];
        }
        int indexOf1 = findFirstBit1(result);
        for (int i : nums) {
            if (isBit1(i, indexOf1)) {
                res[0] ^= i;
            } else {
                res[1] ^= i;
            }
        }
        return res;
    }

    private static boolean isBit1(int num, int indexBit) {
        num >>>= indexBit;
        return (num & 1) == 1;
    }
    private static int findFirstBit1(int num) {
        int index = 0;
        while ((num & 1) == 0 && index < 32) {
            num >>>= 1;
            index++;
        }

        return index;
    }

    private static int help(int[] nums) {
        int result = nums[0];
        for (int i = 1; i < nums.length; i++) {
            result = result ^ nums[i];
        }
        return result;
    }
}

LeetCode

只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,1]

输出: 1

示例 2:

输入: [4,1,2,1,2]

输出: 4

public class Test3 {
    public static void main(String[] args) {
        int[] nums ={4,1,2,1,2};
        System.out.println(singleNumber(nums));
    }
    public static int singleNumber(int[] nums) {
        if(nums == null || nums.length < 1){
            return -1;
        }
        int result = nums[0];
        for (int i = 1; i < nums.length; i++) {
            result = result ^ nums[i];
        }
        return result;
    }
}

只出现一次的数字 II

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,3,2]

输出: 3

示例 2:

输入: [0,1,0,1,0,1,99]

输出: 99

解法1  可以用一个 HashMap 对每个数字进行计数,然后返回数量为 1 的数字就可以了。

public static int singleNumber(int[] nums) {
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        if (map.containsKey(nums[i])) {
            map.put(nums[i], map.get(nums[i]) + 1);
        } else {
            map.put(nums[i], 1);
        }
    }
    for (Integer key : map.keySet()) {
        if (map.get(key) == 1) {
            return key;
        }

    }
    return -1; 
    // 这句不会执行
}

解法2

我们把数字放眼到二进制形式

假如例子是 1 2 6 1 1 2 2 3 3 3, 3 个 1, 3 个 2, 3 个 3,1 个 6

1 0 0 1

2 0 1 0

6 1 1 0

1 0 0 1

1 0 0 1

2 0 1 0

2 0 1 0

3 0 1 1 

3 0 1 1

3 0 1 1     

看最右边的一列 1001100111 有 6 个 1

再往前看一列 0110011111 有 7 个 1

再往前看一列 0010000 有 1 个 1

我们只需要把是 3 的倍数的对应列写 0,不是 3 的倍数的对应列写 1   

也就是 1 1 0,也就是 6。

原因的话,其实很容易想明白。如果所有数字都出现了 3 次,那么每一列的 1 的个数就一定是 3 的倍数。之所以有的列不是 3 的倍数,就是因为只出现了 1 次的数贡献出了 1。所以所有不是 3 的倍数的列写 1,其他列写 0 ,就找到了这个出现 1 次的数。

public int singleNumber(int[] nums) {
    int ans = 0;
    //考虑每一位
    for (int i = 0; i < 32; i++) {
        int count = 0;
        //考虑每一个数
        for (int j = 0; j < nums.length; j++) {
            //当前位是否是 1
            if ((nums[j] >>> i & 1) == 1) {
                count++;
            }
        }
        //1 的个数是否是 3 的倍数
        if (count % 3 != 0) {
            ans = ans | 1 << i;
        }
    }
    return ans;
}

只出现一次的数字 III

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。

示例 :

输入: [1,2,1,3,2,5]

输出: [3,5]

注意:

结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。

你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?

同剑指offer