这道题出自LeetCode,题目如下:
集合 s
包含从 1
到 n
的整数。不幸的是,因为数据错误,导致集合里面某一个数字复制了成了集合里面的另外一个数字的值,导致集合 丢失了一个数字 并且 有一个数字重复 。
给定一个数组 nums
代表了集合 S
发生错误后的结果。
请你找出重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。
示例 1:
输入: nums = [1,2,2,4]
输出: [2,3]
示例 2:
输入: nums = [1,1]
输出: [1,2]
这道题可以利用二进制位运算的性质巧妙解决。我们假设丢失的数字和重复的数字为a和b。由题目可知,我们有两个序列,一个序列A为[1,2,3,...,a,...,b,...,n],还有一个序列B为[1,2,3,...,b,b,...,n]。我们知道异或运算是满足相同为0不同为1的性质的,一个数异或它自身一定为0。因此,我们可以将两个序列合在一起求一次异或:
A ^ B = a ^ b
我们发现两个序列异或之后的结果就是我们要找的两个数字的异或。让我们再次回到异或的性质中来,两个数异或,相同的二进制位为0,不相同的二进制为1。由于a和b肯定是不相同,那么,a ^ b必定不为0,也就是说它存在一个为1的二进制位。我们假设a ^ b的结果为c。那么,让我们来寻找c最低的二进制为1的位:
d = c & ~(c - 1)
这个也比较好理解,我们假设c的二进制表示为x...x10...0,这里写出来的1就是它最低二进制位为1的位。那么,c - 1的二进制表示为x...x01...1,对其取反得到:~(x...x)10...0,我们可以发现1左边的高位数字一定是原先1左边的高位数字的取反,因此两者求与即可得到:d = 0...010...0,1就是要找的最低二进制为1的位。
那么,有了这个之后,又能做什么呢?我们知道,这个d是可以区分开a和b的,即a和b中,一定有一个数对应的二进制位为0,另外一个为1。回到我们前面说的两个序列,这个d一定可以把两个序列进行划分,一边是二进制位为0的,一边是二进制位为1的,而且a和b一定位于两边。显然,如果这两边各自异或,最后剩下来的,一定一边是a,一边是b。
有了a和b之后,我们需要弄明白哪个是重复的数字,哪个是缺失的数字。这个很简单,只需要遍历一遍包含重复数字的序列,就知道答案了。最后通过的代码如下:
class Solution {
public:
vector<int> findErrorNums(vector<int>& nums) {
int diff = 0;
for(int i = 0; i < nums.size(); i++)
{
diff ^= (i + 1);
diff ^= nums[i];
}
int pivot = diff & ~(diff - 1);
int ga = 0, gb = 0;
for(int i = 0; i < nums.size(); i++)
{
if((i + 1) & pivot)
{
ga ^= (i + 1);
}
else
{
gb ^= (i + 1);
}
if(nums[i] & pivot)
{
ga ^= nums[i];
}
else
{
gb ^= nums[i];
}
}
vector<int> res;
for(int i = 0; i < nums.size(); i++)
{
if(ga == nums[i])
{
res = {ga, gb};
return res;
}
}
res = {gb, ga};
return res;
}
};