一个整型数组 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
该题目的简化版本:一个整型数组 nums 里除 一个 数字之外,其他数字都出现了两次。
1、沿用简化版本中的思路,最直观的就是暴力枚举搜索解决,但时间复杂度不满足要求。
2、使用 哈希表 / 哈希集 统计字符出现的次数,进行保存,二次查找即可。此方式额外申请了n的空间,不满足题目要求。
3、 题目要求时间复杂度是O(n),空间复杂度是O(1)。—— 使用异或解决。
-
如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?
-
全元素进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 0,不同结果为 1。那么在计算过程中,成对出现的数字的所有位会两两抵消为 0,最终得到的结果就是那个出现了一次的数字。
-
- 上述方法怎么扩展到找出两个出现一次的数组?
- 如果可以把所有数字分成两组,使得:两个只出现一次的数字在不同的组中;相同的数字会被分到相同的组中。
- 对两个组分别进行异或操作,即可得到答案的两个数字。
所以现在的问题变为:如何对数据进行分组,从而将两个数分到不同的数组中?
- 重复的数字进行分组。只需要有一个统一的规则,就可以把相同的数字分到同一组了。例如:奇偶分组。因为重复的数字,数值都是一样的,一定会分到同一组。
- 异或满足交换律。
- 第一步异或,相同的数其实都抵消了,剩下两个不同的数异或的结果。
- 这两个数异或结果肯定有某一位为1,不然都是0的话就是相同数。—— 整数 x⊕y 某二进制位为 1 ,则 x 和 y 的此二进制位一定不同。
- 找到这个位,不同的两个数一个在此位为0,另一个为1。按此位将所有数分成两组,分开后各自异或,相同的两个数异或肯定为0(而且分开的时候,两个数必为一组)。
- 剩下的每组里就是要找的结果。
- 难点主要在于对mask的理解。
- mask是一个二进制数,且其中只有一位是1,其他位全是0,比如000010,表示我们用倒数第二位作为分组标准,倒数第二位是0的数字分到一组,倒数第二位是1的分到另一组。
- 如何得到这个mask?分组的目的是将两个不重复数字分开,这两个不重复数字的二进制表示肯定是不同的,但是没必要一位一位地比较,可以从右到左(最低位的1),找到第一个不相同的位,将mask当中这一位变成1,就得到了mask。
- 比如 [2,2,3,3,4,6] 中,不重复的两个数字是 4 、6 ,4(100) 和 6(110) 的异或结果(也是整个数组的异或结果)是 010,表示从右到左,第一次出现不同是在倒数第二位,那么可以确定,mask的倒数第二位是1,其他位是0,即010。
算法流程:
1、遍历 nums 执行异或:
设整型数组 nums = [a, a, b, b, ..., x, y],对 nums 中所有数字执行异或,得到的结果为 x⊕y即:
2、循环左移计算 mask :
根据异或运算定义,若整数 x⊕y 某二进制位为 1 ,则 x 和 y 的此二进制位一定不同。即,找到 x⊕y 某位 1 的二进制位,即可将数组 nums 拆分为上述的两个子数组。根据与运算特点,可知对于任意整数 a 有:
若 a&0001=1 ,则 a 的第一位为 1 ;
若 a&0010=1 ,则 a 的第二位为 1 ;
……
因此,初始化一个辅助变量 mask = 1 (0001) ,通过与运算从右向左循环判断,获取整数 x⊕y 首位 1 ,记录于 m 中,代码如下:
// mask 循环左移一位,直到 xXORy & mask != 0
while(xXORy & mask == 0){
mask <<= 1;
}
3、拆分 nums 为两个子数组:
4、分别遍历两个子数组执行异或:
通过遍历判断 nums 中各数字和 mask 做与运算的结果,可将数组拆分为两个子数组,并分别对两个子数组遍历求异或,则可得到两个只出现一次的数字,代码如下:
for(int num : nums){
// 若 num & m == 0 , 划分至子数组 1 ,执行遍历异或
// 若 num & m != 0 , 划分至子数组 2 ,执行遍历异或
if(num & mask == 0){
x ^= num;
}else{
y ^= num;
}
// 对两组数组异或结束,返回只出现一次的数字
return new int[] {x, y};
}
5、返回最终结果
优先级 | 运算符 | 结合性 |
---|---|---|
1 | ()、[]、{} | 从左向右 |
2 | !、+、-、~、++、-- | 从右向左 |
3 | *、/、% | 从左向右 |
4 | +、- | 从左向右 |
5 | «、»、>>> | 从左向右 |
6 | <、<=、>、>=、instanceof | 从左向右 |
7 | ==、!= | 从左向右 |
8 | & | 从左向右 |
9 | ^ | 从左向右 |
10 | | | 从左向右 |
11 | && | 从左向右 |
12 | || | 从左向右 |
13 | ?: | 从右向左 |
14 | =、+=、-=、*=、/=、&=、|=、^=、~=、«=、»=、>>>= | 从右向左 |
class Solution {
public int[] singleNumbers(int[] nums) {
// 对整体遍历异或
int appOnlyOnceNum1 = 0;
int appOnlyOnceNum2 = 0;
int mask = 1;
int num1XORnum2 = 0; // 两者的异或结果
for(int num : nums){
num1XORnum2 ^= num;
}
// 根据最后两者的异或结果计算掩码
while((num1XORnum2 & mask) == 0){
mask <<= 1;
}
// 根据求得的掩码,对数组进行划分分别求解
// 划分标准:与掩码 与 之后的结果是否为 0
for(int num : nums){
if((num & mask) == 0){
appOnlyOnceNum1 ^= num;
}else{
appOnlyOnceNum2 ^= num;
}
}
return new int[] {appOnlyOnceNum1, appOnlyOnceNum2};
}
}