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

题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof

题目要求

一个整型数组 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^2))是不可行的。因此,考虑使用异或操作来实现。

由于相等的两个数按位异或之后的值为0,那么将数组中所有的数字异或操作起来,所得的数就是我们要寻找的两个落单数字的按位异或结果。
例如:

4 ^ 1 ^ 4 ^ 6 => 1 ^ 6
6 对应的二进制: 110
1 对应的二进制: 001
1 ^ 6 二进制: 111

然而,我们只知道异或的结果,无法反推出这两个数。这时,就需要转换一下思路。如果能将这两个数区分开来,即分到两组里,再分别与两组里其它数异或,就可以得出这两个数分别是什么了。例如:

4 1 4 6 5 5分为4 1 4 和 6 5 5 两组。分别异或,4 ^ 1 ^ 4=>1,6 ^ 5 ^5=>6。这时,我们就找到了落单的1和6两个数字。

PS:由于分组规则是一定的,那么重复数字一定会分到同一组,例如两个4不可能处于不同组。

难点在于如何制定分组规则,区分开两个落单数字。
联想一下,当我们对奇偶数进行分组时,用最后一位二进制是否为1来作为规则。那么,我们可以得出:通过 & 运算来判断一位数字不同即可分为两组
那么如何找出哪一位不同呢?这就可以用上我们刚才求得的异或结果了。

6 对应的二进制: 110
1 对应的二进制: 001
1 ^ 6 二进制: 111

可以看出,对于6和1这两个数而言,任意一位都可以作为区分的依据,因为它们的每一位都不同。即:001、010、100都可以作为区分规则。例如:
6 & 2(即010)=> 010
1 & 2(即010)=> 000
010这个值称为mask。
那么,我们使用 nums[i] & mask == 0 这个判据,就可以将 6 和 1 分入两组了。C语言中,0为假,非0为真,不是非0即1的概念。因此注意一定是 == 0 ,而不是 != 1。

最后,再将两组的异或结果存入函数返回的数组中即可。

代码如下:

#include <stdio.h>
#include <stdlib.h>

/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* singleNumbers(int* nums, int numsSize, int* returnSize){
	*returnSize = 2;
	int num = 0;
	for (int i = 0; i < numsSize; i++){
		num ^= *(nums + i);	//先求出异或值
	}
	int mask = 1;
	while ((num & mask) == 0){ //寻找异或值哪一位为1,可以用作mask
		mask <<= 1;
	}
	int a = 0;
	int b = 0;
	for (int i = 0; i < numsSize; i++){
		if ((*(nums + i) & mask) == 0){	//使用mask作为依据对所有数据进行分组
			a ^= *(nums + i);
		}
		else{
			b ^= *(nums + i);
		}
	}
	int* ret = (int *)malloc(8);	//题设注释要求使用动态数组返回
	*(ret) = a;
	*(ret + 1) = b;
	return ret;
}

int main() {

	int nums[6] = { 15, 16, 21, 22, 22, 21 };
	int numSize = sizeof(nums) / sizeof(nums[0]);
	int size1 = sizeof(int)* numSize;
	int returnSize = 0;
	int* returnSizep = &returnSize;

	int* res = singleNumbers(nums, numSize, returnSizep);

	printf("%d\n", *res);
	printf("%d\n", *(res + 1));

	system("pause");
	return 0;
}

这里另外记录2个细节,也是我初次接触LeetCode遇到的问题:
1、注释中assume caller calls free()表示不需要手动释放开辟的空间。由于之前没有接触过caller的概念,起初有些困惑。
2、函数的参数returnSize表示返回的数组的大小,需要在函数中手动设置为2。若没有赋值,会导致输出有误,无法输出如[6,1],而是只能输出一个]。

上一篇:Leetcode 剑指 Offer 56 - I / 56 - II


下一篇:唱翻天