参考博文:http://blog.csdn.net/wdkirchhoff/article/details/44466659
问题描述
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
分析
数组示例:{1, 1, 2, 2, 3, 3, 4, 4, 5, 6}
首先希望先把出现两次的数字去掉,这需要用到异或运算的一个性质:任何一个数字异或它自己都等于0。遍历一遍数组,将所有数字都异或一遍,得到的结果就是两个只出现一次的数字异或的结果:
arr := []int{1, 1, 2, 2, 3, 3, 4, 4, 5, 6}
result := arr[0]
for i := 1; i < len(arr); i++ {
result ^= arr[i]
}
此时,result的结果就是5 ^ 6的结果:3。
然后,希望通过result找到这两个数字。因为result是异或的结果,还是需要从异或运算的一些性质中找出思路。如果某一位上的二进制数不同,异或结果为1(1 ^ 0 == 1),再看上面的result:
0101
^ 0110
= 0011
也就是说result中的两个1表示5和6的二进制中最后两位不同,可以根据这个从数组中找出这两个数。假设我们知道5和6右起第一位不同,那么通过判断右起第一位是不是0就可以将数组中所有数字分成两个子数组,一个子数组中右起第一位为0,另一个右起第一位为1,即将一个数 & 1,判断是不是等于0。现在两个子数组中都包含一个只出现一次的数字和成对的出现两次的数字(出现两次的数字肯定相等,那么 &1 后要么都是0,要么都是1),之后再使用异或运算将出现两次的数字去掉就可以了。
那么现在还有一个问题就是怎么让result的二进制中只出现1个1呢?
我们希望将除右起第一位1之外的其他位都置0:
以 11100
为例,右起第一位在中间,可以先 11100 - 1
,得到 11011
,此时第3位之后都变成了1,第3位变成0,第3位前面的保持不变,再 ^11100
, 因为第3位之前的没变,异或结果为0,第3位之后的都为1,最后 &11100
,就只保留第3位和之后的,因为第3位之后都为0,所以最后结果就只剩下第3位。
总结:11100 & (11100 ^ (11100 - 1)) == 00100
还有一种做法是:11100 & (-11100) == 00100
,这是利用负数的补码除符号位和右起第一个1之外其他位都取反的性质,通过相与将取反的位变成0。
分析的有些啰嗦了,想把想到的东西都写下来,还真有点难。
代码
arr := []int{1, 1, 2, 2, 3, 3, 4, 4, 5, 6}
result := arr[0]
for i := 1; i < len(arr); i++ {
result ^= arr[i]
}
// 此时result是两个只出现一次的数异或的结果
b := result & -result // 只保留result中右起第一个1,其他位置0
x, y := 0, 0
for i := 0; i < len(arr); i++ {
if b&arr[i] != 0 { // 分组
x ^= arr[i] // 消去出现两次的数
} else {
y ^= arr[i]
}
}
fmt.Println("x=", x, " y=", y) // x= 5 y= 6