只出现一次的数字(三种情况)

文章介绍了如何在给定整数数组中,通过异或运算找出只出现一次的数字,包括不同场景下的解决方案,如O(n)时间复杂度的查找缺失数字和只出现一次的数字I、II、III。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、消失的数字

在介绍只出现一次的数字之前,先介绍一下消失的数字这个题目,

数组nums包含从0n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?

示例 1:

输入:[3,0,1]
输出:2

示例 2:

输入:[9,6,4,2,3,5,7,0,1]
输出:8

思路:该题要求在O(n)时间内完成,使用遍历方法的时间复杂度是O(n^2),不符合题目要求。假设数组int nums[]={0,1,2,3,4,5,6,7,9},中间缺了数字8,先使用num=0与数组中所有元素做异或运算,然后将运算的结果再与[0,N]中的元素挨个做异或运算,得到的结果就是那个消失的数字。

原理:0^0^1^2^3^4^5^6^7^9^0^1^2^3^4^5^6^7^8^9,根据异或运算的结合率上述表达式可写为:0^0^0^1^1^2^2^3^3^4^4^5^5^6^6^7^7^8^9^9,由于相同的数字做异或运算的结果为0,而0与任何数做异或运算的结果还为原数字,所以运算的结果为8,即为消失的那个数字。代码如下:

int missingNumber(int* nums, int numsSize){
    //解题思路:拿整数0与数组nums以及0~n中所有的整数做异或运算
    //得到的结果就是那个缺失的整数

    assert(nums);

    int ret=0;
    for(int i=0;i<numsSize;++i)
    {
       ret^=nums[i];
    }

    for(int j=0;j<=numsSize;++j)
    {
        ret^=j;
    }
    return ret;
}

2、136 只出现一次的数字 I

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

示例 1 :

输入:nums = [2,2,1]
输出:1

示例 2 :

输入:nums = [4,1,2,1,2]
输出:4

示例 3 :

输入:nums = [1]
输出:1

提示:

  • 1 <= nums.length <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104
  • 除了某个元素只出现一次以外,其余每个元素均出现两次。

这道题和消失的数字类似,比较简单,直接将数组中的元素逐个做异或运算,运算结果就是那个只出现一次的数据。代码如下:

int singleNumber(int* nums, int numsSize) {
    assert(nums);

    int ret=nums[0];
    for(int i=1;i<numsSize;i++)
    {
        ret^=nums[i];
    }
    return ret;
}

2、LCR 004.只出现一次的数字  II

假如数组int nums[]={1,1,1,2,2,2,3};数组中各元素的二进制位如下标所示:

十进制二进制
10001
10001
10001
20010
20010
20010
30011

int类型变量的大小是4个字节,也就是32个比特位。由于相同的数的二进制序列都是一样的,因此当我们遍历nums数组当中的数字,依次统计每个比特位出现1的次数时,会出现以下几种情况:

1、某一比特位出现1的次数为0,表明数组nums中的所有数字的该比特位都为0,因此只出现一次的数字的该比特位也为0;

2、某一比特位出现1的次数对3取余后得到0值,表明数组nums中只出现一次的数字的该比特位为0。
3、某一比特位出现1的次数对3取余后得到非0值,表明数组nums中只出现一次的数字的该比特位为1,并且可以断定此时该比特位出现的次数对3取余后实际得到的就是1,而这个余下来的1就是由只出现一次的数字提供的。

因此当我们遍历数组nums统计每个比特位出现1的次数之后,就可以将这32个值依次对3进行取余操作,最终得到的32个0/1序列就是只出现一次的数字的二进制序列。

代码如下:

int singleNumber(int* nums, int numsSize){
    int res=0;
    int total=0;
    for(int i=0;i<32;i++){
        total=0;
        for(int j=0;j<numsSize;j++){
            total+=((nums[j]>>i)&1);
        }
        if(total%3)res|=(1u<<i);
    }
    return res;
}

3、260.只出现一次的数字 III

给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

你必须设计并实现线性时间复杂度的算法且仅使用常量额外空间来解决此问题。

示例 1:

输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:[5, 3] 也是有效的答案。

示例 2:

输入:nums = [-1,0]
输出:[-1,0]

示例 3:

输入:nums = [0,1]
输出:[1,0]

思路:由136只出现一次的数字题目可知,数组中只有1个数字出现了一次,其余数字均出现了两次,将数组中的数字逐个做异或运算,运算结果就是那个只出现1次的那个数字。但是该题目中有两个数字只出现了一次,其余数字均出现了两次,显然该方法行不通。

假如有数组int nums[]={1,2,3,4,5,6,1,2,3,4};可以将该数组中的数据分成两组,一组是(1,1,2,2,5),另一组是(3,3,4,4,6),这样将两组中的数据逐个做异或运算,得到的两个结果就是两个只出现一次的数据。

但是该如何去分组呢?

首先使用数字0与数组中的数据挨个做异或运算,得到的结果就为两个只出现一次的数据做异或运算的结果。

根据以上方法的分组结果如下表所示:

数组中的元素二进制序列第一个二进制位
100000000 00000000 00000000 000000011
100000000 00000000 00000000 000000011
200000000 00000000 00000000 000000100
200000000 00000000 00000000 000000100
300000000 00000000 00000000 000000111
300000000 00000000 00000000 000000111
400000000 00000000 00000000 000001000
400000000 00000000 00000000 000001000
500000000 00000000 00000000 000001011
600000000 00000000 00000000 000001100

分组的结果为:

第一个二进制位为1的组:1  1  3  3  5;

第一个二进制位位0的组:2  2  4  4  6;

最后分别逐个对这两组中的数据做异或运算,得到的结果就是两个只出现一次的数据。

以上只是举了一个特殊的例子,真正写代码时,要寻找两个只出现一次的数字做异或运算的结果在第几个二进制位上是1。此时,可以借助异或运算的结果与1做按位与运算,如果运算的结果位0,1做左移运算,直到运算的结果为1,此时记录1左移了几位。如果1左移了M位,则就以M位上的二进制数位依据分组,二进制位为1的分为一组,二进制位为0的分为一组。如下所示:

代码如下:

int* singleNumber(int* nums, int numsSize, int* returnSize) {
    //1、使用0与数组的数据进行异或操作,
    //最后得到两个只出现一次的数据异或运算的结果
    int ret=0;
    for(int i=0;i<numsSize;++i)
    {
        ret^=nums[i];
    }

    //2、两个只出现一次的数据异或运算的结果与1做按位与运算,
    //如果结果为0,则1做左移运算,直到两个按位与运算的结果为1,
    //此时,计算1左移了几位,得到两个只出现1次的元素异或运算的结果
    //从第几位开始二进制位上的数据是不同的,依次作为分组依据。
    int m=0;
    while(m<32)
    {
        if(ret&(1<<m))
            break;
        else
            ++m;
    }

    //将两个只出现1次的数据分到变量x1和变量x2中
    int ret1=0;
    int ret2=0;
    for(size_t j=0;j<numsSize;++j)
    {
        if(nums[j]&(1<<m))
            ret1^=nums[j];
        else
            ret2^=nums[j];
    }

    int* retArray = (int*)malloc(2*sizeof(int));
    retArray[0] = ret1;  retArray[1] = ret2;
    *returnSize = 2;

    return retArray;
}

以上代码运算出现了一个错误:

为什么会出现这个错误?

错误翻译过来就是:1 左移31 位的运算结果不能用类型“int”表示,这就是左移发生了越界的情况,详细情况可见文章位运算之左移(<< )-优快云博客,该怎么解决该问题呢?将int类型的数字转换为无符号类型的size_t就解决该问题了。

正确代码如下:

int* singleNumber(int* nums, int numsSize, int* returnSize) {
    //1、使用0与数组的数据进行异或操作,
    //最后得到两个只出现一次的数据异或运算的结果
    int ret=0;
    for(int i=0;i<numsSize;++i)
    {
        ret^=nums[i];
    }

    //2、两个只出现一次的数据异或运算的结果与1做按位与运算,
    //如果结果为0,则1做左移运算,直到两个按位与运算的结果为1,
    //此时,计算1左移了几位,得到两个只出现1次的元素异或运算的结果
    //从第几位开始二进制位上的数据是不同的,依次作为分组依据。
    int m=0;
    while(m<32)
    {
        if(ret&((size_t)1<<m))
            break;
        else
            ++m;
    }

    //将两个只出现1次的数据分到变量x1和变量x2中
    int ret1=0;
    int ret2=0;
    for(size_t j=0;j<numsSize;++j)
    {
        if(nums[j]&((size_t)1<<m))
            ret1^=nums[j];
        else
            ret2^=nums[j];
    }

    int* retArray = (int*)malloc(2*sizeof(int));
    retArray[0] = ret1;  retArray[1] = ret2;
    *returnSize = 2;

    return retArray;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值