文章目录
- 找出只出现一次的数
- 引入
- Leetcode 260
- Leetcode 137
找出只出现一次的数
对于数组中有一类题,即某些数据在数组中只出现一遍,需要我们找出,今天我们来看看这个类型的题。
引入
想必大家应该见过这么一道题:
现给定一个数组,这个数组里面只有一个数字出现一遍,其他的数据均出现两遍,请找出只出现一次的那个数字。
这种题是有很多方法做的:
1.暴力查找
直接从头开始定位,往后查找看是否有重复数据。如果有就定位下一个位置。反之返回当前的数据。这个方法很简单,也最好想。但是时间复杂度为O(N^2),当数据个数较多时效率较低。
2.进行统计排序
在学习了八大排序算法后知道了统计排序的优势。先对数据出现次数进行统计,然后对统计数组查找个数为1的那个数据即可。但是时间复杂度为O(N)。
所以我们就在想,有没有办法,能够在线性时间复杂度的情况下还能做到常数级别的空间。
3.答案就是使用我们不太熟悉的按位异或计算
在这之前复习一下按位异或的知识点:
按位异或^,即对数据的补码进行每一个比特位异或操作。
如果该位相同,结果为0,相异为1.
所以就得到如下几个结论:
表达式 | 结果 |
---|---|
0 ^ a | a |
a ^ a | 0 |
a ^ b ^ a | a ^ a ^ b |
所以对于上述数组,我们可以先用一个值int ans去与数组中所有元素进行异或操作。由表格我们知道,每两个一样的数据进行异或是会变成0,而0和其他数异或结果不变。所以当我们对数组中每个数据都进行异或的时候,如果数组中只有一个元素出现一次,那么ans的值一定就是只出现一次的那个数字。
Leetcode 260
原题链接:Leetcode 260
这题改动一下,一个数组中有两个数字只出现一次,其他都是出现两次,且要求要线性时间复杂度和常数级别的空间复杂度。
那我们再来考虑一下异或的操作,定义变量int Xornum = 0,假设数组中只出现一次的数据分别是x1和x2,那么最后x的值就是x = x1 ^ x2。
现在就需要想办法将x1和x2从Xornum = x1 ^ x2中分开。
但是这个思路比较难想到,我们一起来看学习一下(我也是学习的):
1.先找到Xornum这个数二进制序列的最低位的1
这里我们先不管原理,先就这么做,找到Xornum的最低位1。
(注意:所有的对二进制码的操作都是对数据的补码进行操作)
应该怎么找呢?
答案就是让Xornum和-Xornum这两个数据进行按位与操作,我们举个例子来看看:
以五为例子,我们知道5的最低位1就是从右往左数第一个位。通过Xornum & -Xornum的操作我们成功的找到了其最低位1,并且这个序列其他位全为0。我们设Xornum & -Xornum得到的结果放在变量int one_pos中。
但是有一件事情要注意,这个Xornum的是int类型,范围在-231 ~ 231 - 1间。对于1 - 231 ~ 231 - 1之间的数据,直接用这个方法就可以求,因为是可以找到对应的相反数的二进制序列。而对于-231这个有符号整形的最小值则不能这么求。
我们之前学过,10000000是-128的补码,其实也是它的源码和反码。这是特殊情况。对于-231 来讲,10000000 00000000 00000000 00000000就是它的补码。最高位实在第一个位置的。所以对于Xornum == -231 的情况,需要特殊处理一下。
所以最后得到表达式int one_pos = (Xornum == INT_MIN) ? Xornum : Xornum & -Xornum
,其中INT_MIN是有符号整形的最小值。
2.分类操作
然后就进行一个分类的操作:
我们得到了Xornum的最低位的那个1(one_pos的那个1),而Xornum又是由x1和x2按位异或出来的。也就是说Xornum的那一位的1(假设是从右往左数第L位),其实是由x1和x2从右往左数第L位按位异或出来的。
为了方便叙述,我们称从右往左第L位为第L位。
抑或出来为1,那么也就是说,x1的第L位和x2的第L位一定是不同的,一定是一个为0,一个为1。这还是很好理解的。
也就是说,x1和x2的第L位和one_pos的第l位的情况是:一个相同那么另外一个就不相同。
那我们就可以依次进行分类了:(以其补码第L位是否和one_pos的第L位是否相同)
对于原数组中,出现两次的那些数据,不管它们被分在哪一类,两个数字是一样的,那肯定是一类,那就放在一起。而只出现一次的两个数据一定是被分在两类的。
3.对不同分类中的数据进行异或
然后我们把不同类的中的所有数据异或起来,那么出现两次的那些数据异或后直接变成0了。那么当不同分类中的数据异或完成后,剩下的自然是只出现两次的数据了。
我们可以举一个例子看看:
这样子就很轻松的将两个只出现一次的数据分离出了。
最后来看看代码实现:
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
vector<int> v(2, 0);
int xornum = 0;
for(int x : nums){
xornum ^= x;
}
int INTMIN = (-1) * pow(2,31);
int one_pos = (xornum == INTMIN) ? xornum :xornum & (-xornum);
for(int x : nums){
if(x & one_pos)
v[0] ^= x;
else
v[1] ^= x;
}
return v;
}
};
Leetcode 137
原题链接:Leetcode 137
这题对于引入的例子也是修改了一点:只有一个数字出现一次,其他的会出现3次,找出只出现一次的数据,并且要求线性时间复杂度,常数级别空间复杂度。
这题我们再来延续使用以下异或的思想,我们会发现很困难。因为其他数据出现三次,而异或操作只能抵消偶数个。如果按照刚刚的思想来分类也是不太可能的,因为分类的情况下,可能出现一次的数据和出现三次的数据会被分到一块,这是很难办的。
有没有别的办法呢?
答案是有的,我们可以按位处理。
设只出现一次的数据位int ans = 0;。假设我们从低位到高位求出ans的比特位:
对于每一位,无非就是0或者1,因为只有一个数据出现一次,其他的出现三次。那么其他的数据在该位上出现的0或者1一定是3的整数倍次。
很好理解,假设数组[3, 3, 3, 4, 4, 4, 1],假设当前求的是第一位(从右往左数),3的第一位是1,出现三次,4的第一位是0,出现三次。很明显都是三的倍数。而1的第一位是1,出现一次。所以整个数据第一位出现1四次,出现0三次。
以此类推,我们很容易得知:对于整个数组来说,第i位出现的0和1的次数一定是一个为3的倍数,一个比3的整数倍还多一个。多出来的那一个可以用取模操作得到是什么。那么就是ans的第i为的二进制位。
而一个int类型数据也就是32个比特位,所以只需要求32次ans的对应比特位即可。这个时间复杂度是线性的。
然后就是要将二进制位依次填充到ans中:
这个应该如何操作呢?
我们是从右往左依次获取二进制位的。
我们来找一下规律:
以此类推,只需要执行上述操作32次即可。哪怕第i位是0也是执行上述操作,只不过是把0进行移位后再按位或。
所以来看看代码实现:
class Solution {
public:
int singleNumber(vector<int>& nums) {
//确定每一个二进制位
int ans = 0;
//求单独出现的数的二进制补码序列 逆序
int i = 0;
while(i < 32){
int OneNum = 0;
for(int x : nums){
if((x >> i) % 2) ++OneNum;
}
OneNum %= 3;
ans = ans | (OneNum << i);
++i;
}
return ans;
}
};
这里非常巧妙的求出第i位二进制序列是1还是0。算出数组中所有数字第i位1的出现次数(Onenum),如果是3的倍数,那么ans的这一位就是0,正好是Onenum取余3的结果。反之如果不是3的倍数,那么ans这一位就是1,也正好是Onenum取余3的结果。
至此就将这题也讲解完了。