一、题目描述
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是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]
限制:
2 <= nums.length <= 10000
二、思路分析
注:思路分析中的一些内容和图片参考自力扣各位前辈的题解,感谢他们的无私奉献
思路
题目要求时间复杂度O(N),空间复杂度 O(1),因此首先排除暴力法和哈希表统计法。
异或
我们先将问题简化一下:一个整型数组 n u m s nums nums 里除一个数字之外,其他数字都出现了两次,如何找到这个数字呢?
设整型数组 n u m s nums nums 中出现一次的数字为 x x x,出现两次的数字为 a , a , b , b , . . . a, a, b, b, ... a,a,b,b,...,即 n u m s = [ a , a , b , b , . . . , x ] nums=[a,a,b,b,...,x] nums=[a,a,b,b,...,x]。异或运算有个重要的性质,两个相同数字异或为0 ,即对于任意整数 a a a 有 a ⊕ a = 0 a \oplus a = 0 a⊕a=0。因此,若将 n u m s nums nums 中所有数字执行异或运算,留下的结果则为出现一次的数字 x x x,即:
a ⊕ a ⊕ b ⊕ b ⊕ . . . ⊕ x = 0 ⊕ 0 ⊕ . . . ⊕ x = x \begin{aligned} & \ \ a \oplus a \oplus b \oplus b \oplus ... \oplus x \\ = & \ \ 0 \oplus 0 \oplus ... \oplus x \\ = & \ \ x \end{aligned} == a⊕a⊕b⊕b⊕...⊕x 0⊕0⊕...⊕x x
异或运算满足交换律 a ⊕ b = b ⊕ a a \oplus b = b \oplus a a⊕b=b⊕a,即以上运算结果与 n u m s nums nums 的元素顺序无关
设 n u m s = [ 3 , 3 , 4 , 4 , 1 ] nums = [3, 3, 4, 4, 1] nums=[3,3,4,4,1],以上计算流程如下图所示
本题难点: 数组 n u m s nums nums 有两个只出现一次的数字,因此无法通过异或直接得到这两个数字。
思路:设两个只出现一次的数字为 x x x、 y y y,由于 x ≠ y x \ne y x=y,则 x x x 和 y y y 二进制至少有一位不同(即分别为0和1)。例如 x = 3 = 0011 x=3=0011 x=3=0011, y = 2 = 0010 y=2=0010 y=2=0010,此时 x ⊕ y = 0001 x \oplus y=0001 x⊕y=0001,可以看出第1位不同。如果有多个不同的位,我们只需找到最低的不相同的位置。这个不相同很关键,意味着我们可以将 n u m s nums nums 中的所有数按照 第1位的取值(0,1)分成两组,那么 x x x 与 y y y 必然不在同一组里,同时这两组里面除了 x x x 和 y y y,其余的数都是成对的。最后再对两组分别进行异或,那么两组各自异或的结果就是 x x x、 y y y。
算法流程:
①遍历 n u m s nums nums 执行异或:
设整型数组 n u m s = [ a , a , b , b , . . . , x , y ] nums = [a, a, b, b, ..., x, y] nums=[a,a,b,b,...,x,y],对 n u m s nums nums 中所有数字执行异或,得到的结果为 x ⊕ y x \oplus y x⊕y,即:
a ⊕ a ⊕ b ⊕ b ⊕ . . . ⊕ x ⊕ y = 0 ⊕ 0 ⊕ . . . ⊕ x ⊕ y = x ⊕ y \begin{aligned} & \ \ a \oplus a \oplus b \oplus b \oplus ... \oplus x \oplus y \\ = & \ \ 0 \oplus 0 \oplus ... \oplus x \oplus y \\ = & \ \ x \oplus y \end{aligned} == a⊕a⊕b⊕b⊕...⊕x⊕y 0⊕0⊕...⊕x⊕y x⊕y
②循环左移计算 m m m
根据异或运算定义,若整数 x ⊕ y x \oplus y x⊕y 某二进制位为1,则 x x x 和 y y y 的此二进制位一定不同,要找到这一位在哪里。根据与运算特点,可知对于任意整数 a a a 有:
若 a & 0001 = 1 a \& 0001 = 1 a&0001=1,则 a a a 的第一位为1
若 a & 0010 = 1 a \& 0010 = 1 a&0010=1,则 a a a 的第二位为1
以此类推……
因此,初始化一个辅助变量 m = 1 m = 1 m=1,去与上 x ⊕ y x \oplus y x⊕y 的结果。如果结果为0,则让 m m m 左移一位,继续去与。然后循环此过程,直到获取整数 x ⊕ y x \oplus y x⊕y 的首位1,记录于 m m m 中
③拆分 n u m s nums nums 为两个子数组:找到 x ⊕ y x \oplus y x⊕y 某为 1 的二进制位,即可将数组 n u m s nums nums 拆分为两个子数组(其中一组那一位是1,另外一组是0)
④分别遍历两个子数组执行异或
⑤返回值
案例分析:
设 n u m s = [ 3 , 3 , 4 , 4 , 1 , 6 ] nums = [3, 3, 4, 4, 1, 6] nums=[3,3,4,4,1,6],以上计算流程如下图所示
复杂度分析:
时间复杂度 O ( N ) \rm{O(N)} O(N):线性遍历nums
使用O(N)
时间,遍历 x ⊕ y x \oplus y x⊕y 二进制位使用O(32)=O(1)
时间
空间复杂度 O ( 1 ) \rm{O(1)} O(1):辅助变量a
、b
、x
、y
使用常数大小额外空间。
三、整体代码
整体代码如下
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* singleNumbers(int* nums, int numsSize, int* returnSize){
int* res = (int*)malloc(sizeof(int)*2);
//因为相同的数字异或为0,任何数字与0异或结果是其本身。
//所以遍历异或整个数组最后得到的结果就是两个只出现一次的数字异或的结果:即 z = x ^ y
int z = 0;
for(int i = 0; i < numsSize; i++) z ^= nums[i];
//我们根据异或的性质可以知道:z中至少有一位是1,否则x与y就是相等的。
//我们通过一个辅助变量m来保存z中哪一位为1.(可能有多个位都为1,我们找到最低位的1即可)。
//举个例子:z = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位为1.
//我们将m初始化为1,如果(z & m)的结果等于0说明z的最低为是0
//我们每次将m左移一位然后跟z做与操作,直到结果不为0.
//此时m应该等于1000,同z一样,第四位为1.
int m = 1;
while((z&m)==0) m<<=1;
//我们遍历数组,将每个数跟m进行与操作,结果为0的作为一组,结果不为0的作为一组
//例如对于数组:[1,2,10,4,1,4,3,3],我们把每个数字跟1000做与操作,可以分为下面两组:
//nums1存放结果为0的: [1, 2, 4, 1, 4, 3, 3]
//nums2存放结果不为0的: [10] (碰巧nums2中只有一个10,如果原数组中的数字再大一些就不会这样了)
//此时我们发现问题已经退化为数组中有一个数字只出现了一次
//分别对nums1和nums2遍历异或就能得到我们预期的x和y
int x = 0, y = 0;
for(int i = 0; i < numsSize; i++){
//这里我们是通过if...else将nums分为了两组,一边遍历一遍异或。
//跟我们创建俩数组nums1和nums2原理是一样的。
if((nums[i]&m) == 0) x^=nums[i];
else y^= nums[i];
}
res[0] = x;
res[1] = y;
*returnSize = 2;
return res;
}
运行,测试通过