题目: 一个整型数组 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
这个题目有没有很熟悉,和我们上次做的136. 只出现一次的数字的思路是一样的,都需要用到位运算⊕,上一个题目是找出现一次的,那么全部⊕即可得结果,这个需要找到两个不一样的数字,我们来看一下解题思路。
一、错误解法:双指针(超出时间限制),哈希表(空间限制)
哈希表肯定是不行的,因为空间复杂度不能超过O(1),我当时想着可不可以用双指针即快慢指针的办法实现O(N)的时间复杂度,结果是不行的,不能通过,时间在某些测试用例上达到了O(N^2),没有通过所有测试用例,这里我分享一下双指针思路,但是不能在力扣上通过,自己测试还是可以的,因为没有时间限制:
- 为了达到O(1)的空间复杂度,我引入变量fir,sed分别保存第一个找到的数字和第二个找到的数字,最后清空存储数据的数组,把这两个变量压入数组,返回即可。
- 可以计算出有几对重复数据,用count表示,count=(nums.size()-2)/2
- 设置low,fast指针,low负责循环,fast负责在数组中找和fast一样的,fast++如果可以找到,那么将nums[low],nums[fast]数值置为-1,表示为已经为重复数字。然后继续找一下个数字是否有重复的:low++,fast=low+1,count–;
- 如果fast循环了整个数组都没有找到,那么fast到达数组末尾时,表示low指向的数字只出现了一次,那么low++,fast=low+1。
- 如果low指向的值为-1,表示它已经是出现两次的数字了,跳过low++,fast=low+1
- 最后将数组中不等于-1的值给fir,sed变量,数组清空,压入数组,返回数组。
//快慢指针O(N^2)时间复杂度不符合
vector<int> singleNumbers(vector<int>& nums)
{
if(nums.size()==0)
return nums;
int low=0;
int fast=1;
int count=(nums.size()-2)/2;//算出有几对其他数字
int fir=-1;
int sed=-1;
while(low<nums.size()-1)//到倒数第二位即可,因为fast指向最后一位,可以进行比较
{
if(nums[fast]==nums[low] ||fast==nums.size()-1||nums[low]==-1)//找到重复数字,到达数组末尾,指向已经判断重复的数字
{
if(nums[fast]==nums[low])
{
count--;
nums[low]=-1;
nums[fast]=-1;
}
low++;
fast=low+1;
continue;
}
if(count==0)//表示已经找到所有偶数数字了
{
break;
}
fast++;
}
for(int i=0;i<nums.size();i++)
{
if(nums[i]==-1)
continue;
if(fir==-1)//将不为-1的数值给这两个变量,
{
fir=nums[i];
}
else//表示fir已经保存了第一个数字
{
sed=nums[i];
}
}
nums.clear();
nums.push_back(fir);
nums.push_back(sed);
return nums;
}
int main()
{
vector<int> nums;
nums.push_back(1);
nums.push_back(2);
nums.push_back(10);
nums.push_back(4);
nums.push_back(1);
nums.push_back(2);
nums.push_back(3);
nums.push_back(3);
nums=singleNumbers(nums);
for(int i=0;i<nums.size();i++)
{
cout<<nums[i]<<" ";
}
}
二、正确解法:位运算
求解找到只出现一次的数字,我们可以利用⊕的性质来求解,这个题目也可以,我们试想如何将找2个不重复数字分解为两组,一组找一个,这就要对数组进行正确的分组,分组就会有两种情况:
- 不重复的两个数组在一组,⊕结果不为0,重复的数组在一组⊕为0。
- 不重复的数字分别在两组,两组⊕结果都不为0,那么结果就是这两组的结果。
很显然,我们就需要将数组分为第二种分组的办法,但是应该以什么标准去划分才可以将两个不重复的数字分别划分到一组,这就需要有一个正确的标准对数组进行划分,然后将两组分别⊕即可,相同的为0,最后就剩下只出现一次的数字,两组结果就是答案。
我们先来看一下如果两个不同的数字⊕的结果,如1⊕5=001⊕101=100,两个数字的二进制第一次出现不一样的地方如何找呢?
我们可以设置flag=1,那么如果⊕结果sum【&】flag不等于0,就表示出现了1,如果为0,那么我们将flag左移即可,我们得到1和5二进制在第2位出现了不一样,那么flag=100,故我们可以用flag和1,5的【&】的结果来区分这两个数字,那么我们就根据flag将数组划分,最后两组分别⊕得到的就是答案,如下图:
那么我们总结思路就是:
- 先循环得到数组整体⊕值sum。
- 再根据sum和flag的&,<<操作得到flag,表示两个出现一次的数字的二进制出现不一样的位置。
- 用flag和数组所有的值,进行&,将flag&nums[i]=0分到一组,fir^=nums[i]得到第一组的结果,其它!=0的为另一组,sed^=nums[i]得到第二组结果。
- 最后fir,sed就是我们要找的两个数字,放入数组,返回数组。
//2.位运算
vector<int> singleNumbers(vector<int>& nums)
{
if(nums.size()==0)
return nums;
int sum=nums[0];//记录整体⊕结果
for(int i=1;i<nums.size();i++)
{
sum^=nums[i];
}
int flag=1;//划分标准
while((flag&sum)==0)//找到这两个数字首先出现不一样的地方
{
flag=flag<<1;
}
int fir=0;
int sed=0;
for(int i=0;i<nums.size();i++)
{
if(nums[i]&flag)//和flag&为1的分为一组,这样肯定将两个元素分为了两组
{
fir^=nums[i];
}
else
{
sed^=nums[i];
}
}
nums.clear();
nums.push_back(fir);
nums.push_back(sed);
return nums;
}
这样时间为O(N),空间为O(1),符合要求。
加油哦!💪。