剑指Offer:[第22天 位运算(中等)]--->数组中数字出现的次数


一、题目描述

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


二、思路分析

注:思路分析中的一些内容和图片参考自力扣各位前辈的题解,感谢他们的无私奉献

思路

题目要求时间复杂度O(N),空间复杂度 O(1),因此首先排除暴力法和哈希表统计法。
异或
我们先将问题简化一下:一个整型数组 n u m s nums nums 里除一个数字之外,其他数字都出现了两次,如何找到这个数字呢?
设整型数组 n u m s nums nums 中出现一次的数字为 x x x,出现两次的数字为 a , a , b , b , . . . a, a, b, b, ... a,a,b,b,...,即 n u m s = [ a , a , b , b , . . . , x ] nums=[a,a,b,b,...,x] nums=[a,a,b,b,...,x]。异或运算有个重要的性质,两个相同数字异或为0 ,即对于任意整数 a a a a ⊕ a = 0 a \oplus a = 0 aa=0。因此,若将 n u m s nums nums 中所有数字执行异或运算,留下的结果则为出现一次的数字 x x x,即:
   a ⊕ a ⊕ b ⊕ b ⊕ . . . ⊕ x =    0 ⊕ 0 ⊕ . . . ⊕ x =    x \begin{aligned} & \ \ a \oplus a \oplus b \oplus b \oplus ... \oplus x \\ = & \ \ 0 \oplus 0 \oplus ... \oplus x \\ = & \ \ x \end{aligned} ==  aabb...x  00...x  x
异或运算满足交换律 a ⊕ b = b ⊕ a a \oplus b = b \oplus a ab=ba,即以上运算结果与 n u m s nums nums 的元素顺序无关
n u m s = [ 3 , 3 , 4 , 4 , 1 ] nums = [3, 3, 4, 4, 1] nums=[3,3,4,4,1],以上计算流程如下图所示
在这里插入图片描述
本题难点: 数组 n u m s nums nums 有两个只出现一次的数字,因此无法通过异或直接得到这两个数字。
思路:设两个只出现一次的数字为 x x x y y y,由于 x ≠ y x \ne y x=y,则 x x x y y y 二进制至少有一位不同(即分别为0和1)。例如 x = 3 = 0011 x=3=0011 x=3=0011 y = 2 = 0010 y=2=0010 y=2=0010,此时 x ⊕ y = 0001 x \oplus y=0001 xy=0001,可以看出第1位不同。如果有多个不同的位,我们只需找到最低的不相同的位置。这个不相同很关键,意味着我们可以将 n u m s nums nums 中的所有数按照 第1位的取值(0,1)分成两组,那么 x x x y y y 必然不在同一组里,同时这两组里面除了 x x x y y y,其余的数都是成对的。最后再对两组分别进行异或,那么两组各自异或的结果就是 x x x y y y
算法流程:
①遍历 n u m s nums nums 执行异或:
设整型数组 n u m s = [ a , a , b , b , . . . , x , y ] nums = [a, a, b, b, ..., x, y] nums=[a,a,b,b,...,x,y],对 n u m s nums nums 中所有数字执行异或,得到的结果为 x ⊕ y x \oplus y xy,即:
   a ⊕ a ⊕ b ⊕ b ⊕ . . . ⊕ x ⊕ y =    0 ⊕ 0 ⊕ . . . ⊕ x ⊕ y =    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} ==  aabb...xy  00...xy  xy
②循环左移计算 m m m
根据异或运算定义,若整数 x ⊕ y x \oplus y xy 某二进制位为1,则 x x x y y y 的此二进制位一定不同,要找到这一位在哪里。根据与运算特点,可知对于任意整数 a a a 有:
a & 0001 = 1 a \& 0001 = 1 a&0001=1,则 a a a 的第一位为1
a & 0010 = 1 a \& 0010 = 1 a&0010=1,则 a a a 的第二位为1
以此类推……
因此,初始化一个辅助变量 m = 1 m = 1 m=1,去与上 x ⊕ y x \oplus y xy 的结果。如果结果为0,则让 m m m 左移一位,继续去与。然后循环此过程,直到获取整数 x ⊕ y x \oplus y xy 的首位1,记录于 m m m
③拆分 n u m s nums nums 为两个子数组:找到 x ⊕ y x \oplus y xy 某为 1 的二进制位,即可将数组 n u m s nums nums 拆分为两个子数组(其中一组那一位是1,另外一组是0)
④分别遍历两个子数组执行异或
⑤返回值
案例分析:
n u m s = [ 3 , 3 , 4 , 4 , 1 , 6 ] nums = [3, 3, 4, 4, 1, 6] nums=[3,3,4,4,1,6],以上计算流程如下图所示
在这里插入图片描述
复杂度分析:
时间复杂度 O ( N ) \rm{O(N)} O(N):线性遍历nums使用O(N)时间,遍历 x ⊕ y x \oplus y xy 二进制位使用O(32)=O(1)时间
空间复杂度 O ( 1 ) \rm{O(1)} O(1):辅助变量abxy使用常数大小额外空间。


三、整体代码

整体代码如下

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* singleNumbers(int* nums, int numsSize, int* returnSize){
    int* res = (int*)malloc(sizeof(int)*2);
    //因为相同的数字异或为0,任何数字与0异或结果是其本身。
    //所以遍历异或整个数组最后得到的结果就是两个只出现一次的数字异或的结果:即 z = x ^ y
    int z = 0;
    for(int i = 0; i < numsSize; i++) z ^= nums[i];
    //我们根据异或的性质可以知道:z中至少有一位是1,否则x与y就是相等的。
    //我们通过一个辅助变量m来保存z中哪一位为1.(可能有多个位都为1,我们找到最低位的1即可)。
    //举个例子:z = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位为1.
    //我们将m初始化为1,如果(z & m)的结果等于0说明z的最低为是0
    //我们每次将m左移一位然后跟z做与操作,直到结果不为0.
    //此时m应该等于1000,同z一样,第四位为1.
    int m = 1;
    while((z&m)==0) m<<=1;
    //我们遍历数组,将每个数跟m进行与操作,结果为0的作为一组,结果不为0的作为一组
    //例如对于数组:[1,2,10,4,1,4,3,3],我们把每个数字跟1000做与操作,可以分为下面两组:
    //nums1存放结果为0的: [1, 2, 4, 1, 4, 3, 3]
    //nums2存放结果不为0的: [10] (碰巧nums2中只有一个10,如果原数组中的数字再大一些就不会这样了)
    //此时我们发现问题已经退化为数组中有一个数字只出现了一次
    //分别对nums1和nums2遍历异或就能得到我们预期的x和y
    int x = 0, y = 0;
    for(int i = 0; i < numsSize; i++){
        //这里我们是通过if...else将nums分为了两组,一边遍历一遍异或。
        //跟我们创建俩数组nums1和nums2原理是一样的。
        if((nums[i]&m) == 0) x^=nums[i];
        else y^= nums[i];
    }

    res[0] = x;
    res[1] = y;
    *returnSize = 2;
    return res;
}

运行,测试通过
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值