一、题目
一个数组中只有两个数字是出现一次,其他所有数字都出现了两次。
编写一个函数找出这两个只出现一次的数字。
例如:
有数组的元素是:1,2,3,4,5,1,2,3,4,6
只有5和6只出现1次,要找出5和6.
二、思路
1、异或(^)的性质:
#进行异或的两个二进制位:相同为0,相异为1
(1)两个相同的数字进行异或结果为0,即a^a = 0,如:3^3 → 011^011 = 000 = 0
(2)任何非0数与0异或,结果还是非0数本身,即a^0 = a,如:3^0 → 011^000 = 011 = 3
(3)异或支持交换律,即a^b^a = a^a^b =(再利用性质(1)(2)化为) 0^b = b,验证:如:3^5^3 → 011^101^011 =(正常顺序计算)110^011 = 101 = 5
2、根据以上异或的三条性质可知,一个数组中如果只有两个数字各出现一次,那么用0将数组中的所有元素累计地异或一遍,得到的结果即为两个只出现一次数字异或的结果,如:arr[10] = {1,2,3,4,5,1,2,3,4,6},用0累计异或数组中所有元素得到 0^1^2^3^4^5^1^2^3^4^6,根据性质(3)的交换律可将该式变换为0^1^1^2^2^3^3^4^4^5^6,
根据性质(1)可化简为0^0^0^0^0^5^6 → 0^5^6
根据性质(2)得0^5^6 →5^6(得到两个只出现一次数字异或后的结果)
3、用0累计异或数组中所有元素得到的5^6 = 101^110 = 011,由此可知这两个数字第1、2位的二进制位一定是不同的,我们可以只看两个数第1个不同的二进制位,即第1位,我们可以通过a&1看结果是否为1将两个数字区分开(&按位与:进行按位与的两个二进制位都为1时才为1,其余情况均为0)即5&1 = 101&001 = 001 = 1,6&1 = 110&001 = 000 = 0,如此根据按位与1后的结果将两个数字区分开。根据这个方法我们将这个数组所有的元素全部按位与1,那么这个数组就被分成各含一个只出现一次数字的两个小组,同时该数组中出现两次的元素也会被分配到同一小组中,如
按位与1结果为1的组:001(1)、011(3)、101(5)、001(1)、011(3)
按位与1结果为0的组:010(2)、100(4)、010(2)、100(4)、110(6)
4、根据上述方法,我们就可以利用异或的性质(1)(2)(3)分别找到两个小组中只出现一次的数字
三、代码实现
#include<stdio.h>
void Find(int arr[], int sz)
{
int num1 = 0;
int num2 = 0;
int ret = 0;
int n = 0;
int i = 0;
for (i = 0; i < sz; i++)//得到两个只出现一次的两个数字异或后的结果
{
ret = ret ^ arr[i];
}
for (n = 0; n < 32; n++)
{
if (((ret >> n) & 1) == 1)//该循环的目的是找到ret(两个数异或后的结果)二进制序列中第一个值为1的二进制位,
break; //以便于后续按位与1时能够区分两个数
}
for (i = 0; i < sz; i++)
{
if (((arr[i] >> n) & 1) == 1)//数组中的元素全部按位与1从而使其分为两个小组
num1 = num1 ^ arr[i];//1^3^5^1^3 = 5
else
num2 = num2 ^ arr[i];//2^4^2^4^6 = 6
}
printf(" num1=%d\n num2=%d\n", num1, num2);
}
int main()
{
int arr[] = { 1, 2, 3, 4,5 , 1, 2, 3, 4, 6 };
int sz = sizeof(arr) / sizeof(arr[0]);
Find(arr, sz);
return 0;
}