剑指 Offer 56 - I. 数组中数字出现的次数

一个整型数组 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

该题目的简化版本:一个整型数组 nums 里除 一个 数字之外,其他数字都出现了两次。

1、沿用简化版本中的思路,最直观的就是暴力枚举搜索解决,但时间复杂度不满足要求。

2、使用 哈希表 / 哈希集 统计字符出现的次数,进行保存,二次查找即可。此方式额外申请了n的空间,不满足题目要求。

3、 题目要求时间复杂度是O(n),空间复杂度是O(1)。—— 使用异或解决。

  • 如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?

    • 全元素进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 0,不同结果为 1。那么在计算过程中,成对出现的数字的所有位会两两抵消为 0,最终得到的结果就是那个出现了一次的数字。

  • 上述方法怎么扩展到找出两个出现一次的数组?
    • 如果可以把所有数字分成两组,使得:两个只出现一次的数字在不同的组中;相同的数字会被分到相同的组中。
    • 对两个组分别进行异或操作,即可得到答案的两个数字。

所以现在的问题变为:如何对数据进行分组,从而将两个数分到不同的数组中?

  • 重复的数字进行分组。只需要有一个统一的规则,就可以把相同的数字分到同一组了。例如:奇偶分组。因为重复的数字,数值都是一样的,一定会分到同一组。
  • 异或满足交换律。
    • 第一步异或,相同的数其实都抵消了,剩下两个不同的数异或的结果。
    • 这两个数异或结果肯定有某一位为1,不然都是0的话就是相同数。—— 整数 x⊕y 某二进制位为 1 ,则 x 和 y 的此二进制位一定不同。
    • 找到这个位,不同的两个数一个在此位为0,另一个为1。按此位将所有数分成两组,分开后各自异或,相同的两个数异或肯定为0(而且分开的时候,两个数必为一组)。
    • 剩下的每组里就是要找的结果。
  • 难点主要在于对mask的理解。
    • mask是一个二进制数,且其中只有一位是1,其他位全是0,比如000010,表示我们用倒数第二位作为分组标准,倒数第二位是0的数字分到一组,倒数第二位是1的分到另一组。
    • 如何得到这个mask?分组的目的是将两个不重复数字分开,这两个不重复数字的二进制表示肯定是不同的,但是没必要一位一位地比较,可以从右到左(最低位的1),找到第一个不相同的位,将mask当中这一位变成1,就得到了mask。
    • 比如 [2,2,3,3,4,6] 中,不重复的两个数字是 4 、6 ,4(100) 和 6(110) 的异或结果(也是整个数组的异或结果)是 010,表示从右到左,第一次出现不同是在倒数第二位,那么可以确定,mask的倒数第二位是1,其他位是0,即010。

算法流程:
1、遍历 nums 执行异或:
        设整型数组 nums = [a, a, b, b, ..., x, y],对 nums 中所有数字执行异或,得到的结果为 x⊕y即:

\begin{aligned} & \ \ a \oplus a \oplus b \oplus b \oplus ... \oplus x \oplus y \\ = & \ \ 0 \oplus 0 \oplus ... \oplus x \oplus y \\ = & \ \ x \oplus y \end{aligned}

2、循环左移计算 mask :
        根据异或运算定义,若整数 x⊕y 某二进制位为 1 ,则 x 和 y 的此二进制位一定不同。即,找到 x⊕y 某位 1 的二进制位,即可将数组 nums 拆分为上述的两个子数组。根据与运算特点,可知对于任意整数 a 有:

        若 a&0001=1 ,则 a 的第一位为 1 ;
        若 a&0010=1 ,则 a 的第二位为 1 ;
        ……
        因此,初始化一个辅助变量 mask = 1 (0001) ,通过与运算从右向左循环判断,获取整数 x⊕y 首位 1 ,记录于 m 中,代码如下:

// mask 循环左移一位,直到 xXORy & mask != 0
while(xXORy & mask == 0){
    mask <<= 1;
}

3、拆分 nums 为两个子数组:
4、分别遍历两个子数组执行异或:

        通过遍历判断 nums 中各数字和 mask 做与运算的结果,可将数组拆分为两个子数组,并分别对两个子数组遍历求异或,则可得到两个只出现一次的数字,代码如下:

for(int num : nums){
    
    // 若 num & m == 0 , 划分至子数组 1 ,执行遍历异或
    // 若 num & m != 0 , 划分至子数组 2 ,执行遍历异或
    if(num & mask == 0){
        x ^= num;
    }else{
        y ^= num;
    }
    
    // 对两组数组异或结束,返回只出现一次的数字
    return new int[] {x, y};
}

5、返回最终结果 

Picture2.png

运算符优先级
优先级运算符结合性
1()、[]、{}从左向右
2!、+、-、~、++、--从右向左
3*、/、%从左向右
4+、-从左向右
5«、»、>>>从左向右
6<、<=、>、>=、instanceof从左向右
7==、!=从左向右
8&从左向右
9^从左向右
10|从左向右
11&&从左向右
12||从左向右
13?:从右向左
14=、+=、-=、*=、/=、&=、|=、^=、~=、«=、»=、>>>=从右向左

class Solution {
    public int[] singleNumbers(int[] nums) {
        // 对整体遍历异或
        int appOnlyOnceNum1 = 0;
        int appOnlyOnceNum2 = 0;
        int mask = 1;
        int num1XORnum2 = 0; // 两者的异或结果

        for(int num : nums){
            num1XORnum2 ^= num;
        }

        // 根据最后两者的异或结果计算掩码
        while((num1XORnum2 & mask) == 0){
            mask <<= 1;
        }

        // 根据求得的掩码,对数组进行划分分别求解
        // 划分标准:与掩码 与 之后的结果是否为 0
        for(int num : nums){
            if((num & mask) == 0){
                appOnlyOnceNum1 ^= num;
            }else{
                appOnlyOnceNum2 ^= num;
            }
        }

        return new int[] {appOnlyOnceNum1, appOnlyOnceNum2};
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值