剑指 Offer 56 - I. 数组中只出现一次的两个数字
一个整型数组 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)$,所以没办法利用两个for
循环暴力的判断每个数字出现的次数,然后返回那两个只出现一次的,其实就算没有时间复杂度的要求,也不应该用两个for
循环的办法,因为时间复杂度会是$O(n^2)$的,比较低效,面试官不会满意的,应该寻求其他解决办法。
题目又要求了空间复杂度是$O(1)$,所以我们也没有办法用一个map
来统计每个数字出现的次数,尽管那样时间复杂度是满足了$O(n)$的要求。
题中很明显在强调除两个数字之外,其他数字都出现了两次,这有什么意义呢?
我们想到异或运算的一个性质
,任何一个数字异或它自己都等于0
。也就是说,如果我们从头到尾依次异或数组中的每个数字,那么最终的结果刚好是那两个不同的数字异或的结果,因为异或运算具有交换律
,那些成对出现两次的数字全部在异或中抵消了。
由于结果中的两个数字肯定不一样,那么异或的结果肯定不为0
,也就是说,在这个结果数字的二进制表示中至少有一位为1
。我们在结果数字中找到第一个为1
的位的位置,记为第k
位。由于这一位是那两个不同的数字异或的结果,说明那两个数字的其中一个第k
位为1
,另一个第k
位为0
。
现在我们以第k
位是不是1
为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第k
位都是1
,而第二个子数组中每个数字的第k
位是0
。由于我们分组的标准是数字中的第k
位是1
还是0
,那么出现了两次的数字肯定被分配到同一个子数组中。而不同的那两个数字分别在两个子数组中。
现在我们可以再一次用到异或的性质,将第一个集合中的所有元素进行异或,最后剩下的就是那个只出现一次的数字first
,而另一个只出现一次的数字用sum^first
就行了。
总结上述过程:
1. 异或得到 sum
2. 取sum第k位为1的数
3. 将数分为两个集合,第k位为1的集合和第k位不是1的集合
4. 其中那两个不同的数字分别在这两个集合,且相同的元素是在同一个集合里面
5. 于是将其转化成了求重复数字中的单个数值的问题
Java代码
class Solution {
public int[] singleNumbers(int[] nums) {
//出现两次的数字异或后都互相抵消了,剩下的就是那两个不同数字异或的结果sum
int sum = nums[0];
for(int i = 1;i < nums.length;i++){
sum ^= nums[i];
}
//找到sum第一个为1的位置
int k = 0;
while((sum >> k & 1) != 1) k++;
//并不需要将数据分为两个集合存起来,所以不违反O(1)空间复杂度的要求
int fisrt = 0;
for(int i = 0;i < nums.length;i++){
if(((nums[i] >> k) & 1) != 0){//第k位为1
fisrt ^= nums[i];
}
}
return new int[]{fisrt,sum ^ fisrt};
}
}