牛客网剑指offer第40题——数组中只出现一次的数字(浅谈位运算的妙用)
题目如下:
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
当我第一次拿到这个题目的时候,我是怎么想的呢?第一步就是对数组排序。因为 排序号相同的元素是相邻的。也就是说。如果某个数字只出现了一次,那么其相邻的两个数据肯定都与它不相同。当然了,如果是第一个数就是,那么其下一个数必然与其不同;如果是最后一个数,那么其上一个数必然也与其不同。
于是我将代码逻辑分成下面的部分:
排序->判断第一个数->判断最后一个数->判断中间的数
那么 还需要解决的一个问题是:针对void FindNumsAppearOnce(vector<int> data,int* num1,int *num2),假如我找到了一个数,我是将这个数据给num1呢,还是给num2呢?,因此我们还需要一个计数器,如果计数器的值为0,则将这个数给num1,并将计数器加1;如果计数器为1,则将这个数给num2,并将计数器加1;之后判断计数器是否为2,如果为2,则直接跳出循环。
具体代码如下:
1 class Solution { 2 public: 3 void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) { 4 if(data.size() <= 3) 5 return; 6 int cnt =0; 7 sort(data.begin(),data.end()); 8 for(int i =0;i < data.size();i++) 9 { 10 if(i == 0) 11 { 12 if(data[i+1] != data[i]) 13 { 14 *num1 = data[i]; 15 cnt++; 16 } 17 } 18 if(i == data.size()-1) 19 { 20 if(data[i-1] != data[i]) 21 { 22 *num2 = data[i]; 23 cnt++; 24 } 25 } 26 else 27 { 28 if((data[i-1]!=data[i]) && (data[i+1]!=data[i])) 29 { 30 if(cnt == 0) 31 { 32 *num1 = data[i]; 33 cnt++; 34 } 35 else if(cnt == 1) 36 { 37 *num2 = data[i]; 38 cnt++; 39 } 40 } 41 } 42 if(cnt == 2) 43 break; 44 } 45 } 46 };
这样做当然是可以的,但存在两个问题,即使是排序,最低的时间复杂度也是O(nlogn),并且,排序必然会导致这两个数的相对顺序可能发生变化,如果题目要求按顺序将第一个给num1,将第二个给num2,此时我们便不能采用排序的做法了。当然了,你也许会说:还有一种做法:直接遍历,当然也是可以的,时间复杂度为O(n2)。
那么有没有更好的做法呢?当然有:位运算,应该说是异或运算。
我们先来看一个比较简单的情况,如果数组中只有一个数字出现一次,其他都出现两次。那么我们应该可以想到异或运算。异或运算有一个比较好的性质是:相同为0,相异为1。也就是说,任何一个数字异或它自己都等于0,而0异或任何数都等于那个数。因此,我们从头到尾依次异或数组中的每个数字,那么最终结果刚好是那个只出现一次的数字,重复的数字在异或过程中被抵消了。
我们可以直接代码来验证:
1 vector<int> vec; 2 vec.push_back(2); 3 vec.push_back(3); 4 vec.push_back(4); 5 vec.push_back(3); 6 vec.push_back(4); 7 int one = 0; 8 for(int i=0;i<vec.size();i++) 9 { 10 one = one^vec[i]; 11 } 12 cout<<"the res of one:"<<one<<endl;
//输出结果为2。
也就说:如果一个数组只有一个元素出现一次,其他元素出现两次。我们可以通过异或将这个元素找出来。原因在于两个相同的元素异或为0,而0异或某个数等于这个数本身!!!(我们应该认识到二进制和十进制并没有本质区别,不能二进制中我们熟悉异或运算,十进制数反而我们不知道这个规律了)。还应该认识到的是:我们可以看到,上述数组vec中相同的元素并未存放在相邻空间,也就是说,异或运算结果和数据的位置是没有关系的。异或运算时满足交换律的。也就是:a^b^c ==b^a^c(上述数组中2^3^4^3^4 = 2^(3^3)^(4^4)= 2)。交换律和结合律对于异或运算同样成立。
回到这个题目,这个题目说的是:存在两个只出现一次的元素,而非是只存在一个这样的元素。那么这个时候该怎么办?
借助上述思路,我们可以进一步分析,如果我们能把数组分成两个子数组,使每个子数组包含一个只出现一次的数字,而其他数字成对出现,那么我们通过上述解法就可以找到两个元素。也就是核心是,将这个数组分成两部分,每部分只包含一个只出现一次的元素,这样对每部分进行异或运算,就能找到。
具体思路是:我们首先仍然从前向后依次异或数组中的数字,那么得到的结果是两个只出现一次的数字的异或结果,其他成对出现的数字被抵消了。由于这两个数字不同,所以异或结果肯定不为0,也就是这个异或结果一定至少有一位是1,我们在结果中找到第一个为1的位的位置,记为第n位。接下来,以第n位是不是1为标准,将数组分为两个子数组,第一个数组中第n位都是1,第二个数组中第n位都是0。这样,便实现了我们的目标。
有人可能还是有些异或,我在这里再做进一步分析,将所有的数异或得到结果,就是将这两个只出现一次的数异或得到的结果,且不为0。至少有一位为1.因为这位为1,所以这两个只出现一次的数,必然有一个数这位为0.另一个这位位1.因此我们按照这个为1的位划分,必然能分成两个数组。这两个只出现一次的数必然再不同的数组。尽管极端情况有可能除去这两个数的其他数全部被分到了一组,但并不影响!因为我们的目的是:将这两个不同的数据分开!有人可能担心,会不会有可能其他出现两次的数被划分到不同的阵营里面??这是完全不可能的。既然是完全相同的两个数,他们的二进制每一位自然是相同的。也就是除去这两个只出现一次的数,其他的数据,必然成对的出现再某一组,不可能两个相同的数被分到了两个不同的组。
至此,可以采用之前的思想采用异或运算分别找出这两个只出现一次的数了,代码如下:
class Solution { public: void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) { if(data.size() <= 3 || data.size()%2) return; int bdder =0; for(int i =0;i < data.size();i++) bdder = bdder^data[i]; int flag=1; while(flag) { if(flag&bdder) break; flag = flag<<1; } *num1 = 0; *num2 = 0; for(int i = 0;i<data.size();i++) { if(flag&data[i]) *num1 = *num1^data[i]; else *num2 = *num2^data[i]; } } };