【LeetCode】一个整型数组里除两个数字之外,其他数字都出现了两次,请找出这两个只出现一次的数字(数组、分组异或)

题目难度:中等

(1)题目描述

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

示例1

输入:nums = [4, 1, 4, 6]

输出:[1, 6] 或 [6, 1]

示例2

输入:nums = [1, 2, 3, 4, 5, 6, 1, 2, 3, 4]

输出:[5, 6] 或 [6, 5]

LeetCode链接:剑指 Offer 56 - I. 数组中数字出现的次数


(2)解题思路:分组异或

  • 先来思考一下这个问题

一个整型数组里出现一次的数只有一个,其他数字都出现了两次,请找出这个只出现一次的数字。

要解决这个问题,利用异或的特性(两数相同异或等于0,0和一个数异或就等于其本身),所以数组中只有一个出现一次的数,那么只需要异或整个数组就可以得到这个只出现一次的数。

  • 回到本题,出现一次的数有两个

假设输入:nums = [1, 2, 3, 4, 5, 6, 1, 2, 3, 4],我们需要从中找到 5 和 6

类比前面问题的解法,异或整个数组,得到的是 5 和 6 的异或结果,显然是不行。

那么我们能不能把要找的这两个数字,分成两组,一组放一个,这样分别异或这两组数,就得到了这两个数字。

  • 分组的要求

1)两个只出现一次的数必须在不同组

2)两个相同的数必须出现在同一组

比如分为(1 3 1 3 5)和(2 4 2 4 6),这样就很容易找到这两个数字了。

  • 如何分组呢?(问题核心)

异或整个数组的结果就是这两个出现一次的数异或的结果,是一定不等于 0 的,那么说明这个异或结果的二进制序列中一定是有 1 的,找到这个 1,我们就可以利用这个来分组

nums = [1, 2, 3, 4, 5, 6, 1, 2, 3, 4],全部异或的结果就是 5 和 6 异或的结果,即 0101 和 0110 异或的结果 0011

0011 中为1 的二进制位表示什么意思呢,分析不难知道,异或相同为 0 ,相异为 1,所以二进制位是 1,就表示 5 和 6 的二进制数在第一、二位上的数是不同的。这就是分组依据。

我们就以第一个为 1 二进制位为分组条件,将数组中第一个二进制位为 1 的数分为一组,第二个二进制位为 0 的数分为另一组,这样就把 5 和 6 成功分到不同的组,而剩下的那些相同的数(1 和 1、2 和 2,3 和 3、4 和 4),因为数值相同,所以它们的第一个二进制位一定是相同的。这样就同时把两个相同的数划分到同一组了。

  • 为了能够更清晰的看到分组情况,我画了一个表格:
数组二进制序列第一个二进制位
100011
100011
200100
200100
300111
300111
401000
401000
501011
601100
  • 分组结果为:

第一个二进制位为 1 的组(1,1,3,3,5)

第一个二进制位为 0 的组(2,2,4,4,6)

然后,分别对两组数异或,就得到出现一次的那两个数。

  • 时间复杂度:O(N),只遍历了数组两次。

  • 空间复杂度:O(1),只需常数的空间存放若干变量。

  • 参考代码如下:

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* singleNumbers(int* nums, int numsSize, int* returnSize){
    //1、异或整个数组,得到两个只出现一次的数字的异或结果
    int ret = 0;
    int i = 0;
    for(i = 0; i < numsSize; i++)
    {
        ret ^= nums[i];
    }
    //2、计算ret的哪一个二进制位为1
    int target = 1;  //0001
    while((target & ret) == 0)  //按位与结果为0,说明ret的第一个二进制位为0
    {
        target = target << 1;  //左移一位,继续去找ret中第一个为1的二进制位
    }
    //循环结束后,target中为1的二进制位在第几位,ret的第一个为1的二进制位就在第几位
    
    //3、根据这一位对所有数字分组
    //并分别对每一组的数字进行异或操作,得到这两个数字
    int a = 0, b = 0;
    for(i = 0; i < numsSize; i++)
    {
        //按位与结果为0,说明nums[i]的那个二进制位为0
        if((target & nums[i]) == 0)  
        {
            a ^= nums[i];
        }
        else  //按位与结果不为0,说明nums[i]的那个二进制位不为0
        {
            b ^= nums[i];
        }
    }
    //4、动态开辟一个存放这两个数字的数组
    int* parr = (int*)malloc(2 * sizeof(int));
    parr[0] = a;
    parr[1] = b;
    *returnSize = 2;
    return parr;
}
  • 补充:计算 ret 的哪一个二进制位为1,并根据此分组,还可以这样写哦
//2、计算ret哪一个二进制位为1
int pos = 0;  //记录ret的第几个二进制位为1
for(i = 0; i < 32; i++)
{
    if(((ret >> i) & 1) == 1)  //不断左移,和1按位与结果为1,找到首个为1的二进制位
    {
        pos = i;  //ret第pos位为1
        break;
    }
}
//3、把从高到底的第pos位为1、为0的数进行分组
int a = 0, b = 0;
for(i = 0; i < numsSize; i++)
{
    //左移pos位,和1按位与结果为1,说明nums[i]第pos位为1
    if(((nums[i] >> pos) & 1) == 1)
    {
        a ^= nums[i];
    }
    else
    {
        b ^= nums[i];
    }
}

大家快去动手试一试吧!

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 25
    评论
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值