1. 题目介绍(56. 数组中数字出现的次数)
面试题56.:数组中数字出现的次数, 一共分为两小题:
- 题目一:数组中只出现一次的两个数字
- 题目二:数组中唯一只出现一次的数字
2. 题目1:数组中只出现一次的两个数字
题目链接:https://leetcode.cn/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof/
2.1 题目介绍
一个整型数组
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
2.2 题解 – 位运算(XOR)-- O(n)
时间复杂度O(n),空间复杂度O(1)
【解题思路】:
该问题是问在一个数组中找出 两个只出现一次的数字 ,那么我们可以先从这个数组中 只有一个数字只出现了一次 开始考虑:
- 首先我们可以想到 异或运算 的一个性质:任何一个数字异或它自己都等于0;也就是说,如果我们从头到尾依次异或数组中的每个数字,那么最终的结果刚好是那个只出现依次的数组,因为那些成对出现两次的数字全部在异或中抵消了(当然,这也是一个前提条件,要求 数组中的其它数字都是出现了两次,而不是三次或其他次,只能是 偶数次 才行)
……
既然,我们有了得到只出现一次数字的办法,那么我们就要想怎么求出两个只出现一次的数字:
- 我们可以试着将 原数组分成两个子数组 ,使得每个子数组包含一个只出现一次的数字,而其它数字都承兑出现两次。只要能够这样拆分成两个数组,那么我们就可以按照前面的办法分别找出两个只出现一次的数字
……
【实现策略】:
- 首先还是先进行无效输入的判断,判断数组长度
nums.length是否小于等于0,如果是,则说明是无效数据;- 定义变量
exclusiveOr,获取数组中所有元素的异或结果(该结果可以等同于 数组中两个唯一只出现一次的数字 的异或结果);- 我们可以根据这个异或结果(
exclusiveOr),去寻找这两个数字是在哪一位开始不同的,即从低位到高位,第一个Bit为1的位置;- 找到这个位置后,我们就可以根据这个位置进行分组了,我们将 该位为1 的数据分为一组,不为1 的分为一组,然后对其进行异或,最后剩下的数字就是我们要找到的两个数字了。
class Solution {
    // Solution1:异或分组和筛选
    public int[] singleNumbers(int[] nums) {
        // 无效输入判断
        if (nums.length <= 0) return null;
        // 将数组中所有元素进行异或
        int exclusiveOr = 0;
        for (int i = 0; i< nums.length; i++) {
            // 异或完成后,一样的会被抵消,只剩下不一样的两个数字,需要我们对其进行分组
            exclusiveOr ^= nums[i];
        }
        int indexBitIs1 = findFirstBitIs1(exclusiveOr);
        // 遍历数组并分组判断
        int[] res = new int[2];
        for (int j = 0; j < nums.length; j++) {
            if (isBit1(nums[j], indexBitIs1)) res[0] ^= nums[j];
            else res[1] ^= nums[j];
        }
        // 循环结束,返回结果
        return res;
    }
    // 从右向左寻找第一位为1的位数
    public int findFirstBitIs1(int num) {
        int indexFirstBitIs1 = 0;
        while ((num & 1) == 0 && (indexFirstBitIs1 < 32)) {
            num = num >> 1;
            indexFirstBitIs1++;
        }
        return indexFirstBitIs1;
    }
    // 判断输入的数字的indexBitIs1位是不是1
    public boolean isBit1(int num, int indexBitIs1) {
        num = num >> indexBitIs1;
        return (num & 1) != 0 ;
    }
}
代码简化:
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. 返回出现一次的数字
    }
}
3. 题目2:
题目链接:https://leetcode.cn/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/
3.1 题目介绍
在一个数组
nums中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
【测试用例】:
 示例1:
输入:nums = [3,4,3,3]
输出:4
示例2:
输入:nums = [9,1,7,9,7,9,7]
输出:1
【条件约束】:
限制:
- 1 <= nums.length <= 10000
- 1 <= nums[i] < 2^31
3.2 题解 – 位运算 – O(n)
时间复杂度O(n),空间复杂度O(1)
【解题思路】:
因为三个相同的数字的异或结果还是该数字,因此我们在这里就不能再使用异或运算解决该问题了,不过我们还是可以沿用位运算的思路:
- 如果一个数字出现三次,那么它的二进制表示的每一位(
0或者1)也出现三次;- 如果把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位的和都能被
3整除;- 我们把数组中所有数字的二进制表示的每一位都加起来,如果某一位的和能被
3整除,那么那个只出现一次的数字二进制表示中对应的那一位是0;否则就是1.……
这种解法的时间效率是 O(n),我们需要一个长度为 32 的辅助数组存储二进制表示的每一位的和。当然,除此之外,还有其它解法:
- 从排序数组中找到只出现一次的数字,但排序需要额外的
O(nlogn)的时间;- 用一个哈希表来记录数组中每个数字出现的次数,但这个哈希表需要额外的
O(n)的空间;- 有限状态自动机:各二进制位的 位运算规则相同 ,因此只需考虑一位即可。如下图所示,对于所有数字中的某二进制位
1的个数,存在3种状态,即对3余数为0,1,2;该方法虽然效率较高,但也较难理解
class Solution {
    // Solution1:位运算
    public int singleNumber(int[] nums) {
        // 定义辅助数组 counts,用来存储二进制表示的每一位的和
        int[] counts = new int[32];
        // 循环遍历数组中的所有数
        for(int num : nums) {
            // 将该数的32位分别存入数组
            for(int j = 0; j < 32; j++) {
                counts[j] += num & 1;
                num = num >> 1;
            }
        }
        int res = 0, m = 3;
        for(int i = 0; i < 32; i++) {
            // 移位处理
            res <<= 1;
            // 如果 某一位的和能被 3 整除,那么那个只出现一次的数字
            // 二进制表示中对应的哪一位是0;否则就是1
            res |= counts[31 - i] % m;
        }
        return res;
    }
}
有限状态自动机:
 
class Solution {
    public int singleNumber(int[] nums) {
        int ones = 0, twos = 0;
        for(int num : nums){
            ones = ones ^ num & ~twos;
            twos = twos ^ num & ~ones;
        }
        return ones;
    }
}
4. 参考资料
[1] 剑指 Offer 56 - I. 数组中数字出现的次数(位运算,清晰图解)


















