题目
一个整型数组 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 <= 10000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof
解题
基础(位运算介绍)
对几种位运算的详解请看Java 位运算
这道题主要应用的是按位异或^和按位与&的性质。
按位异或的性质:相同的数异或结果为0,不同的数异或结果为1。
- 归零律:a ^ a = 0;
- 交换律:a ^ b = b ^ a;
- 恒等律:a ^ 0 = a;
- 结合律:(a ^ b) ^ c = a ^ (b ^ c);
- 自反律:a ^ b ^ b = a ^ 0 = a;
按位与:a & 0 = 0; a & 1 = a; (0&0=0;0&1=0;1&0=0;1&1=1)
解题思路
假设所有数中唯一没有重复的两个数为a和b,求解nums[]中a和b的过程分为两步:
- nums中所有数全部异或:因为所有数中只有两个数是不重复的,其他都有重复配对。根据相同数异或为0的性质,很容易想到我们把所有的数全部异或求得结果就是所求不重复的两个数a和b的异或值a^b。(根据a ^ 0 = a 和 a ^ a = 0)
n1 ^ n1 ^ a ^ n2 ^ n2 ^ … ^ b ^ nn ^ nn = a ^ b 因为ni ^ nI = 0,相当于都相互抵消掉了 - 已知a ^ b的结果怎么分解出a和b?
2-1. 分part: 将整个数组nums[]分为两部分A[]和B[],要求①将a和b分别分布在两个部分;② 相同的数一定在同一个部分内。
这样 A[] = {a,n1,n1,n2,n2,…nk,nk},B[] = {b,nk+1,nk+1,nk+2,nk+2,…nn,nn}
求解: a = A部分内所有数字异或结果,b = B部分内所有数字异或结果。
2-2. 怎么实现这样的分part?
因为a,b不同,所以异或结果restmp中必然会出现至少一个1,将a^b结果中的最低位的“1”求出标记为mask,用每个数在该位上的取值0/1作为分part的依据。
将每个num & mask,根据a&1 = a、a&0 = 0的性质可知求出结果为num在mask为1的那位上的值,在该位上为0的在A部分,该位上位1的在B部分。因为重复的数字在该位上一定是一样的满足了分part要求②,而不重复的a和b在该位上一定是不同的,所以被分别分在了A和B两部分,满足了分part要求①。
示例: 以nums = [4,1,4,6]为例:
-
全部异或求出a ^ b:restmp = 4 ^ 1 ^ 4 ^ 6 = 1 ^ 6 = 0111(7)
-
求标记位mark(异或结果中最低位出现的1,表示a、b不同):mark = 0001
可以用restmp & (-restmp) = mask
也可以依次判断restmp每一位是否为1,mask不断左移来求出restmp最右边(最低位)的1
-
分part: 以mask = 0001,即最低位的1作为分part标准把{4,1,4,6}分为两部分A和B。
A部分:mark & num = 0,说明num的这一位是0:xxx0;{4,4,6}
B部分:其他的num就是这一位是1的:xxx1。{1}
-
分别求A、B部分的元素异或结果
A:a = 4 ^ 4 ^ 6 = 6
B:b = 1
故所求结果为{1,6}
时间复杂度:O(n)
空间复杂度:O(1)
java实现
class Solution {
public int[] singleNumbers(int[] nums) {
//求出全体异或的结果,即为不重复的两个数组的异或结果
int restmp = 0;
int a = 0, b = 0;
for(int num:nums){
restmp^=num;
}
//已知a^b=restmp 怎么从两个数的异或结果中分离出这两个数a和b
//restmp中可以求出至少有一位是不同的即为1,用这个1来将原数组中的每个元素分为两部分
//一部分是该位是1的,一部分是该为是0的。而因为a和b在该位上异或结果是1说明是不同的,所以他肯定被分在了这两部分的不同的部分中,而其他重复的数字在该位上肯定是相同的,要么是1,要么是0,所以肯定在相同的部分
//求用来判断的最低位上的不同,即restmp从低位开始出现的第一个1,用&的性质判断
int mask = restmp & (-restmp);
// while((restmp&mask) == 0){//说明当前mask为1的位上,restmp为0,则寻找下一位
// mask <<=1;//左移一位
// }
//找出mask后,将nums所有元素分为两部分,两部分再分别异或,两部分的异或结果就是所求两个不重复的数字a和b
for(int num:nums){
if((mask&num) == 0){//当前num在mask这位上为0
a ^= num;
}else{
b ^= num;
}
}
return new int[]{a,b};
}
}
参考:力扣官方题解