【中篇启航!】牛客剑指offer JZ56 数组中只出现一次的两个数字

一、题目描述

原题连接: JZ56 数组中只出现一次的两个数字

题目描述:
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

数据范围:2 <= n <= 1000,数组中的每个数的大小0 < val <= 1000000
要求:空间复杂度O(1),时间复杂度O(n)

示例1
输入:[1,4,1,6]
返回值:[4,6]
说明:返回的结果中较小的数排在前面

示例2
输入:[1,2,3,3,2,9]
返回值:[1,9]

二、解题

1、方法1——数组模拟哈希表

1.1、思路分析

由于题目中给出的元素最大为1000000,所以我们可以先创建一个长度为1000001的数组fre,然后对数组array进行遍历,每遍历到一个元素就执行fre[array[i]]++,记录array数组中每个元素出现的频数,然后再次遍历一遍数组array,当fre[array[i]] == 1时,将array[i]写入一个数组(我们可以直接将这两个数写到array[0]和array[1]。
因为这个方法点儿像是在使用哈希表,所以我称它为数组模拟哈希表。

1.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

// 写一个用于比较整型数据的函数
int cmp_int(const void* p1, const void* p2) {
    return *((int*)p1) - *((int*)p2);
}
int* FindNumsAppearOnce1(int* array, int arrayLen, int* returnSize) {
    assert(array && returnSize);
    *returnSize = 0;
    // 先对数组进行排序
    int fre[1000001] = { 0 };
    int i = 0;
    for (i = 0; i < arrayLen; i++) {
        fre[array[i]]++;
    }
    for (i = 0; i < arrayLen; i++) {
        if (1 == fre[array[i]]) {
            array[*returnSize] = array[i];
            *returnSize += 1;
        }
    }
    // 对array的前两个元素进行排序
    qsort(array, 2, sizeof(array[0]), cmp_int);
    return array;
}

时间复杂度:O(n),n为数组元素个数,我们需要遍历两遍次数组。排序的个数是固定的2
空间复杂度:O(1),我们只需要用到常数级的额外空间

2、方法2——排序

2.1、思路分析

我们知道一个数组在排序之后相同的元素一定是紧挨在一起的,例如:
在这里插入图片描述
所以我们可以在排序之后利用两个指针left和right遍历数组,left从左向右遍历,当出现array[left] != array[left + 1]时,将array[left]写入一个数组,right从右向左遍历,当出现array[right] != array[right - 1]时,将array[right]写入一个数组,例如:
在这里插入图片描述
如果相等,left和right在下一次遍历时,都要跳过一个元素(即left = left + 2, right = right - 2),例如:
在这里插入图片描述

2.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int* FindNumsAppearOnce2(int* array, int arrayLen, int* returnSize) {
    assert(array && returnSize);
    *returnSize = 0;
    // 先对数组进行排序
    qsort(array, arrayLen, sizeof(array[0]), cmp_int);
    int left = 0;
    int right = arrayLen - 1;
    while (left <= right) {
        if (array[left] != array[left + 1]) {
            array[*returnSize] = array[left];
            *returnSize += 1;
            left++;
        }
        else {
            left += 2;
        }
        if (left < right) { // 有可能上面已经出现了left > right
            if (array[right] != array[right - 1]) {
                array[*returnSize] = array[right];
                *returnSize += 1;
                right--;
            }
            else {
                right -= 2;
            }
        }
    }
    qsort(array, 2, sizeof(array[0]), cmp_int);
    return array;
}

时间复杂度:O(n),n为数组元素个数
空间复杂度:O(1),我们只需要用到常数级的额外空间

3、方法3——分组异或运算

3.1、思路分析

我们都知道异或运算有以下规律:
a ^ a = 0;
a ^ 0 = a;
那么,假设数组中那两个只出现一次的数分别是m或n,如果我们将数组中所有的数都异或起来,得到的结果是不是就等于m ^ n呢?
但是光得到m ^ n好像也分析不出m和n到底是什么啊?
但若是我们能把n和m分成两组,n所在的组中只有n是只出现一次的其余都出现两次,m所在的组中也是只有m只出现一次,其余都出现了两次。然后对这两组分别进行全异或,是不是就得到了n和m具体是几了呢?
那么怎么将数组中的数分成两组且一组含有n一组含有m呢?
加下来我就给大家推演一下:
如果要我们分辨一个数是奇数还是偶数,熟悉二进制的我们肯定能想到,奇数的二进制序列的最低位肯定是1,而偶数的肯定是0。
那么我们可以将一个数与1按位与运算,如果得到的结果为1,那说明这个数是一个奇数,如果是0,那这个数就是一个偶数。
那这跟我们要将n和m分成两组有什么关系呢?
别急,请让我给大家举个例子,例如8^5 和 4 ^ 9(为了方便,我这里只写int的后八位):
在这里插入图片描述
有没有发现,只要是奇数和偶数相异或,得出的结果无论前面的序列如何不同,但最低位的1一定是相同的。
我们也可以这样理解:对于奇数和偶数只要找到它们二进制序列的第一个不同的位(从右往左),就能将它们以奇偶性”的不同分成两组。
那我们把思维扩展开来,对于任意两个数,是不是都可以找到它们二进制序列的第一个不同的位,就可以将它们以“某种性质”的不同,从而将他们分为两组呢?
当然可以,只是我们并不需要关心那“某种性质”到底是哪一种性质,这里我们需要做的只是分组而已。

3.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int* FindNumsAppearOnce(int* array, int arrayLen, int* returnSize) {
    assert(array && returnSize);
    // 先异或数组中所有的数,得到n ^ m
    int i = 0;
    int n_m = 0;
    for (i = 0; i < arrayLen; i++) {
        n_m ^= array[i];
    }
    int div = 0; // 那个用于分组的数
    // 再找出第一个不同的位
    for (i = 0; i < 32; i++) {
        if (((n_m >> i) & 1) == 1) {
            div = 1 << i;
            break;
        }
    }
    // 取得n和m
    int n = 0;
    int m = 0;
    for (i = 0; i < arrayLen; i++) {
        if ((array[i] & div) == 0) {
            n ^= array[i];
        } else { // 另一种情况就是得到div本身
            m ^= array[i];
        }
    }
    // 将n和m写入array的前两个位置
    *returnSize = 0;
    array[*returnSize] = n;
    *returnSize += 1;
    array[*returnSize] = m;
    *returnSize += 1;
    qsort(array, 2, sizeof(array[0]), cmp_int);
    return array;
}

时间复杂度:O(n),n为数组元素个数,我们至始至终都只是在一层循环中遍历数组。所以时间复杂度为O(n)。
空间复杂度:O(1),我们只需要用到常数级的额外空间。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

林先生-1

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

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

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

打赏作者

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

抵扣说明:

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

余额充值