问题引入
有一个数组:nums = [2, 2, 3, 3, 6, 6] 。
它具有怎样的性质呢?所有元素异或和为0。
这个位运算好像很简单?别着急,接下来,我们将要把位运算发挥到极致。
情境壹——num出现一次,其余元素出现两次
一个数组nums里除某个数字之外,其他数字都出现了两次。找出这个数字。
不假思索,一气呵成。
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for (int num : nums) {
res ^= num;
}
return res;
}
}
情境贰——num1出现一次,num2出现一次,其余元素出现两次
一个数组nums里除两个数字之外,其他数字都出现了两次。找出这两个数字。
此时,我们有些蒙了。因为数组中存在两个只出现一次的数字,所以直接求异或和,一定会得到一个没有意义的结果。
是不是求异或和的思路不能用了呢?
如果可以将原数组拆分为两个子数组,让这两个出现一次的数字分别分到两个数组中,那么是不是就能在两个子数组中分别求异或和、分别得到num1和num2了呢?
问题又来了,如何拆分数组,才能将num1和num2恰好分到两个子数组中?我们需要一个「特征」,根据这个特征,可以将原本相等的数字分到同一个数组中,将num1和num2分到不同的数组中。
为了寻求这个特征,我们回到最初的尝试结果,即所有元素的异或和sum
。所有元素的异或和其实等价于num1 ^ num2
,思考一下,如果在某一位上这个异或值为1说明什么?说明num1和num2在该位上不相等,即一定是一个为0一个为1;而对于相等的其他数字,在该位上自然也是相等的——这不正是我们所寻求的数组拆分特征吗?
根据这个特征,数组拆分为两个子数组。在两个子数组中分别求异或和,也就分别得到了num1和num2。一切迎刃而解!
看着图解理解,非常清晰:
理解了算法的过程,代码自然也不在话下:
class Solution {
public int[] singleNumber(int[] nums) {
// 求所有数字异或和
int sum = 0;
for (int num : nums) {
sum ^= num;
}
// 找异或和第一个为1的位
int mask = 1;
while ((sum & mask) == 0) {
mask <<= 1;
}
// 以该位为依据分组异或
int x = 0;
int y = 0;
for (int num : nums) {
if ((num & mask) == 0) {
x ^= num;
} else {
y ^= num;
}
}
return new int[]{x, y};
}
}
情境叁——num出现一次,其余元素出现三次
一个数组nums里除某个数字之外,其他数字都出现了三次。找出这个数字。
此时,我们又有些蒙了。出现三次的话,使用异或运算是无法消掉的呀!不要着急,跟着下面的思路来:
1)首先,将所有数字都看成 32位 的二进制数;
2)接着,将数组中的所有数字相加。那么,对于"某一位"来说,数值一定是 3N或3N+1 ;
3)为什么?所有出现3次的数字对该位置的贡献,和一定是0或3,出现一次的数字对该位置的贡献,一定是0或1 ;
4)所以,对该位置的和,用3取余,结果就是出现一次的数字在该位置的值。
这种解法是具有通用性的,即出现4次啊、5次啊、N次啊,直接用4、5、N取余即可。
看着图解理解,非常清晰:
理解了算法的过程,代码自然也不在话下:
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for (int i = 0; i < 32; i++) {
// 对于int每一位
int bit = 0;
// 计算该位上的和
for (int num : nums) {
bit += ((num >> i) & 1);
}
// 对3取余即为res在该位的值
res += ((bit % 3) << i);
}
return res;
}
}
感悟总结
当没有头绪的时候,不妨从垂直的角度,以位为单位,一位一位的去分析问题,或许会有意想不到的效果>_< !
E N D END END