题目1:数组中只出现一次的两个数字
题干
一个整形数组里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
示例 1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
示例 2:
输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]
思路
思路1:既然要找只出现一次的数字,我们不妨创建一个数组来统计每个数字出现的次数,但是走到这里就已经违背题干了,因为我们这个数组要多大我们并不能确定,是根据输入确定的,输入n个数数组大小就要是n-1,空间复杂度是O(n),此思路pass掉。
思路2:利用位运算异或,我们都知道异或这种运算满足以下规律
a
^
a
=
0
0
^
a
=
a
(
a
^
b
)
^
c
=
a
^
(
b
^
c
)
a
^
b
=
b
^
a
a^{\hat{}}a=0\\ 0^{\hat{}}a=a\\ (a^{\hat{}}b)^{\hat{}}c=a^{\hat{}}(b^{\hat{}}c)\\ a^{\hat{}}b=b^{\hat{}}a
a^a=00^a=a(a^b)^c=a^(b^c)a^b=b^a
那么如果数组中只有一个不同的数字,其他数字都相同的话,我们可以用0把所有的数组内数字都异或起来,利用交换律和结合律把出现了两次的数字都提前运算一下得到结果0,然后只出现了一次的数字我们就会是这次运算的结果,我们可以用一个图来形象的描绘这一点。
可是问题是这里有两个只出现了一次数字啊,这种情况我们怎么办呢?
我们还是先把所有的数字都异或起来,记这个结果是x,按照我们上面的分析x的结果就是这两个数字的异或。按照位的思想,既然这两个数字不同,那么他们的二进制表示肯定至少有一位是不同的,我们记它们的第一位不同的二进制位的位数是第n位(1<=n<=32),按照异或的性质,x的第n位肯定是1,那么我们可以把数组这样分类:二进制展开下和x的第n位相同的数字我们放在一起,和x的第n位不同的数字我们放在一起,这样分类一定可以把两个不同且只出现了一次数字放到不同的地方去,同时,出现两次的数字一定会被放到同一边,因为他们的二进制位完全相同,这保证了我们可以对这两边分别运用上面讨论的方法。
实现
下面进行C语言实现:
int disbit(int x)
{
int i = 1;
while ((i & x) == 0)
{
i <<= 1;
}
return i;
}
int* singleNumbers(int* nums, int numsSize, int* returnSize)
{
int* array = (int*)malloc(2 * sizeof(int));
array[0] = 0;
array[1] = 0;
int x = 0;
//把x和所有的数都异或上 利用a^a=0 a^0=a的特点 结果肯定是那两个不相同的数字的异或
for (int i = 0; i < numsSize; i++)
{
x ^= nums[i];
}
//它们既然是不同的数字,肯定有一个位不同,第一个不同的位记为第n位 取异或会得1
int m = disbit(x);//获得m为第n位是1,别的位都是0的数
//按照第n为是不是1把数组分成两个部分去取异或。
//因为相同的数字一定是每一位都相同 会出现在同一边 仍可以利用a^a=0
for (int i = 0; i < numsSize; i++)
{
if ((nums[i] & m) == m)
{
array[0] ^= nums[i];
}
else
{
array[1] ^= nums[i];
}
}
*returnSize = 2;
return array;
}
我们可以加上主函数进行测试:
int main()
{
int a[] = { 1,2,5,2 };
int returnsize = 0;
int* p = &returnsize;
singleNumbers(a, 4, p);
for (int i = 0; i < returnsize; i++)
{
printf("%d ", array[i]);
}
return 0;
}
题目2:数组中唯一只出现一次的数字
题干
在一个数组中除一个数字只出现一次外,其他数字都出现了三次。请找出那个只出现了1次的数字。
思路
出现了3次,不能利用异或的性质了,因为那个只对偶数次有效,但是我们仍然按照位的思想,如果我们能把所有出现了3次的数字的第i+1位二进制位加起来放到单独的一个整形变量data[i]中,那么因为它出现了三次,data[i]一定可以整除3,也就是说data[i]%3=0.
上面的思考启示我们,我们如果把所有数字的第i+1位取出然后加起来放到一个整形变量data[i]中,如果那个只出现了一次的数字的第i+1位二进制位是0,那么这个data[i]一定可以整除三;如果那个只出现了一次的数字的第i+1位二进制位是1,那么这个data[i]一定不能整除3,并且这是充要条件,我们可以用data[i]%3是否等于0来判断那个只出现了一次的数字的对应二进制位是0还是1。
现在我们需要用我们求出的只出现了一次的数字的每个位的信息来来求出这个数字,这可以用<<运算实现,一开始是0,然后如果data[31]!=0,那么向这一位写入1,也就是让0+1,然后整个数字<<,不过考虑到我们最后一次输入第一位二进制位以后不应该再左移了,我们可以先<<再写入。
实现
C语言实现如下:
int singleNumber(int* nums, int numsSize)
{
//仍延续位的思想
//如果一个数字出现了三次 那么对于他的每个二进制位来说 把他们加起来的值一定可以被3整除
//所以我们不妨创立一个32个整形的数组data data[i]表示每个数的二进制的第i+1位的求和
//如果能整除3 那么说明这个单独的数字对应的第i+1位数字是0
//如果不能整除3 说明那个单独的数字的第i+1位二进制位的数字是1
int data[32] = { 0 };
unsigned int x = 1;
for (int i = 0; i < 32; i++)
{
for (int j = 0; j < numsSize; j++)
{
//取出nums[j]的第i+1位 并加到data[i]上
if ((nums[j] & x) != 0)
{
data[i]++;
}
}
if(i!=31)
{
x <<= 1;
}
}
//如果data[i]%3==0 那么输出的ret的i+1位是0 否则是1
//我们利用左移完成这点 所以要从31位开始输入
int ret = 0;
for (int i = 31; i >= 0; i--)
{
ret <<= 1;//这个地方应该先移动再插入,防止最后你插入完了最后一位又移动一次导致出错误
if ((data[i] % 3) != 0)
{
ret += 1;
}
}
return ret;
}
加上main函数进行测试
int main()
{
int a[] = { 3,4,3,3 };
printf("%d", singleNumber(a, 4));
return 0;
}
总结:
其实通过这两个算法我们就已经解决了一个数组中有1个只出现了1次的不同数字,其他数字都出现了m次,怎么把这1个数字输出出来的问题和一个数组中有2个只出现了1次的不同数字,其他数字都出现了偶数次,怎么输出这个数字的问题。