问题描述:
一个数组中只有一个数字是出现一次,
其他所有数字都出现了两次。
找出这个只出现一次的数字,编程实现。
思路:
最简单直观的方法,我们用两个for循环,外循环用i,内循环用j表示,数组长度用len表示
外循环执行一次,内循环执行len-1次,逐一比较,如果相等就给count++,外循环一次结束,如果count = 1,说明这个数就出现了一次,打印arr[i]即可
代码示例:
void fond(int* p, int len)
{
int count = 0;
for (int i = 0; i < len; i++)
{
count = 0;
for (int j = 0; j < len; j++)
{
if (p[i] == p[j])
{
count++;
}
}
if (count == 1)
{
printf("%d ", p[i]);
}
}
}
int main()
{
int arr[20];
int len;
printf("请输入数组长度:");
scanf("%d", &len);
printf("请输入数据:");
for (int i = 0; i < len; i++)
{
scanf("%d", &arr[i]);
}
fond(arr, len);
return 0;
}
程序测试:
当然这种方法也可以求数组中任意出现了一次的数字,不过时间复杂度为O(n^2),空间复杂度为O(1),不推荐。
那么有没有更好的方法呢?从节约时间复杂度或空间复杂度着手
当然有的啦
那么这个题的突破口在哪里呢?注意这个数组的特殊性:其它数字都出现了两次,只有一个数出现了一次。可以想到运用异或运算
异或运算有一个性质:任何数异或它自己,结果都是0,而0与任意数按位异或其结果都为该任意数;这样如果题目变成只有一个数字只出现一次,其他数字均出现两次,这样我们从头到尾异或数组中的每一个数字,那么最终的结果就是只出现一次的数字。如此一来,我们的时间复杂度就变成了O(n),空间复杂度为O(1)。
注意:位运算符运算,参与的数字应该为二进制
异或:如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
举个栗子:2 1 3 2 3
2 xor 1 :0010 ^0001 -->0011 ==3
3 xor 3: 0011 ^0011 -->0000==0
0 xor 2: 0000 ^0010 -->0010==2
2 xor 3: 0010 ^0011 -->0001==1 结果为1
代码示例如下:
int GetUnpaired(int* p,int len)//一个整型数组里除了一个数字外,其他的数字都出现了两次
{
int i;
int temp = 0;
for (i = 0; i <len; i++)
{
temp ^= p[i] ;
}
return temp;
}
如果你会了这段代码,那么真正的题目是:一个整型数组里除了两个数字外,其他的数字都出现了两次,要找出这两个数字。这应该怎么写呢?(要求时间复杂度为O(n),空间复杂度为O(1))。
其实思想也不难
思路:
根据上面的讲解,我们知道一个整型数组中,若只有两个数字只出现一次,其他数字都出现了两次,那么把数组中的所有遍历异或一遍的结果就是只出现一次的两个数字相异或的结果。
如果可以把数组中的数字分成两个子数组,每个数组里面包含一个只出现一次的数字,其他的数字都出现两次,这样按照上面的方法就可以分别求出只出现一次的数字了。
那么依照什么条件来将所有数字分成两个数组呢?我们来举一个栗子
我们先定义一个 int arr[] = { 1, 2, 3, 3, 2, 1, 4, 5 };
可以看到,只有4、5两个数只出现了一次,其他数都出现了两次,那么根据上述分析,把该数组中所有的元素都遍历异或一遍所得的结果就是4和5相异或的结果。
把数组中所有的元素分组
为了方便分析,先把数组中所有元素的二进制表示写出来:
1--> 0001
2--> 0010
3--> 0011
4--> 0100
5--> 0101
4和5相异或的结果: 0001
可以看到4和5相异或的结果为0001。我们都知道,两个数相异或的结果中的“1”表示这两个数在该位上不同,就像 4(0100) 和 5(0101)的第一个二进制位不同。
接下来我们按照二进制第一位相同的来划分两个数组
- 第一个二进制位为0
2(0010)、2(0010)、4(0100) - 第一个二进制位为1
1(0001)、 1(0001)、3(0011)、3(0011)、5(0101) - 最后我们通过异或分别找出每一组只出现了一次的数字即可
总结方法:
首先,我们从头到尾异或数组中的每一个数字,那么得到的结果将是这两个只出现了一次的数字异或后的结果(因为其他出现了两次的数字都在异或中抵消了)。由于这两个数字不相同,所以它们的异或结果的二进制表示中至少有一位是1。我们先找到第一个为1的位的位置,。然后我们以这一位是否为1,把原数组分成两个子数组,第一个数组中每个数的这一位都为1,第二个数组中的每个数的这一位都为0,因为两个相同的数字的任意一位都是相同的,所以相同的数肯定分在同一组,这样我们就实现了划分子数组的效果。接下来我们进行代码示例
代码示例:
//根据结果的二进制右数第一个1, 进行分组.在该位上是0的分一组, 是1的分一组
//arr为数组集合,len为数组长度,arg1,arg2分别为指向两个单独数的指针
bool GetUnpairedTwo(int* arr, int len, int* arg1, int* arg2)
{
int temp = 0;
for (int i = 0; i < len; i++)
{
temp ^= arr[i];
}
if (temp == 0)
{
return false;
}
//找到异或结果中最右边为1的下标
int a = 1;
while ((a & temp )==0)
{
a <<= 1;
}
//以a作为分组依据
*arg1 = 0;
*arg2 = 0;
for (int i = 0; i < len; i++)
{
if ((arr[i]& a ) == 0)
{
*arg1 ^= arr[i];
}
else
{
*arg2 ^= arr[i];
}
}
return true;
}
//总结:找到两个数字不一样的那一位,然后分成两类,在每一类中单独去找只出现一次的数字
int main()
{
int arr[20];
int len;
printf("请输入数组长度:");
scanf("%d", &len);
printf("请输入数据:");
for (int i = 0; i < len; i++)
{
scanf("%d", &arr[i]);
}
//fond(arr, len);
int arg1, arg2;
if (GetUnpairedTwo(arr, len, &arg1, &arg2))
{
printf("两个不成对的数字分别是:%d和%d", arg1, arg2);
}
else
{
printf("都成对!");
}
return 0;
}
程序演示:
这样代码就写完啦,走过点个赞