题目
一个整型数组里除了两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
解题思路:
1.如何在一个数组中找到一个只出现一次的数字呢?
- 异或运算的一个性质:任何一个数字异或它自己都等于0。
- 如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字,因为那些成对出现两次的数字全部在异或中抵消了。
举例:
- 比如数组{4,5,5},我们先用数组中的第一个元素4(二进制形式:0100)和数组中的第二个元素5(二进制形式:0101)进行异或操作,0100和0101异或得到0001。
- 再用这个得到的元素与数组中的三个元素5(二进制形式:0101)进行异或操作,0001和0101异或得到0100,正好是结果数字4。
- 这是因为数组中相同的元素异或是为0的,因此就只剩下那个不成对的单独元素。
2.如何在一个数组中找到两个只出现一次的数字呢?
- 我们可以将原数组分成两个子数组,使得每个子数组包含一个只出现一次的数字,而其他数字都成对出现两次。这样,我们就可以用上述同样的方法找到那个单独的元素。
- 我们还是从头到尾依次异或数组中的每个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。
- 因为其他数字都出现了两次,在异或中全部抵消了。由于这两个数字肯定不一样,那么异或的结果肯定不为0,也就是说这个结果数字的二进制表示中至少有一位为1。
- 我们在结果数字中找到第一个为1的位的位置,记为第n位。现在我们以第n位是不是1为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。
举例:
- 比如数组{2,4,3,6,3,2,5,5},我们依次对数组中的每个数字做异或运行之后,得到的结果用二进制表示是0010。
- 异或得到结果中的倒数第二位是1,于是我们根据数字的倒数第二位是不是1将该数组分为两个子数组。第一个子数组{2,3,6,3,2}中所有数字的倒数第二位都是1,而第二个子数组{4,5,5}中所有数字的倒数第二位都是0。
- 接下来只要分别两个子数组求异或,就能找到第一个子数组中只出现一次的数字是6,而第二个子数组中只出现一次的数字是4。
代码
class Solution{
public:
void FindNumsAppearOnce(vector<int> data, int* num1, int* num2){
int length = data.size();
if(length < 2) return;
//对原数组每个元素依次求异或
int resultExclusiveOR = 0; //初始值
for(int i = 0; i < length; i++){
resultExclusiveOR ^= data[i]; //异或
}
//找到原数组异或后结果第一个为1的位数
unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);
//将原数组分为两个子数组
*num1 = *num2 = 0;
for(int j = 0; j < length; j++){
if(IsBit1(data[j], indexOf1)) *num1 ^= data[j];
else *num2 ^= data[j];
}
}
private:
//找到二进制数num第一个为1的位数,比如0010,第一个为1的位数是1。
unsigned int FindFirstBitIs1(int num){
unsigned int indexBit = 0;
//一个字节由8个二进制位组成
while((num & 1) == 0 && (indexBit < 8 * sizeof(unsigned int))){
num = num >> 1; //向右位移
indexBit++;
}
return indexBit;
}
//判断第indexBit位是否为1
bool IsBit1(int num, unsigned int indexBit){
num = num >> indexBit;
return (num & 1);
}
};