问题一:一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数
问题一解题思路
因为a ^ a = 0
, 所以出现过偶次的数异或结果都是0
,又因为0^a=a
,所以把数组中所有的数做异或
以后的结果,就是出现了奇数次的那个数。
问题一完整代码
public class LeetCode_0136_SingleNumber {
public static int singleNumber(int[] nums) {
int ans = nums[0];
for (int i = 1; i < nums.length; i++) {
ans ^= nums[i];
}
return ans;
}
}
问题二:一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数
问题二解题思路
根据问题一的结论,假设数组中a
和b
这两个数出现了奇数次,这个数组中的所有数字异或以后得到的结果一定a^b
因为a
和b
是两种不同的数,所以a^b
的结果一定不等于0。
所以,a^b
的结果如果转换成二进制的话,一定有某位是1。我们假设a^b
转换成二进制后最右侧位置的1在i位置,由此可以得出一个结论:a和b的二进制在i位置一定一个为0,一个为1
不妨假设a
的i
位置为0
,b
的i
位置为1
。
此外,容易得知,整个数组中的数,i
位置为0
的数除了a
以外,其他数一定有偶数个, i
位置为1
的数除了b
之外,其他数一定有偶数个。
那么我们可以只对i
位置为1
的数求异或,最后得到的值一定是b
,然后通过b^(a^b) = a
,可以得到a
的值。
最后只剩下一个问题:
如何求一个数最右侧的1呢?
假设 某个数x二进制为:00010010
, 其最右侧的1是:00000010
。
算法是:对于一个数x
来说,它最右侧的1
等于x & ((~x) + 1)
或者x & (-x)
所以,如果一个数是a^b
,那么它最右侧的1
就是(a^b) & (~(a^b) + 1)
我们用(a^b) & (~(a^b) + 1)
这个值去和数组中每个值数做与运算(&),如果与完以后的结果是0
,说明这个数i
位置是0
,否则说明这个数i
位置是1
。我们前面已经得到一个结论,i
位置为0
的数除了a
以外,其他数一定有偶数个。所以,用(a^b) & (~(a^b) + 1)
这个值和每个i
位置是0
的数组元素做与运算以后,最后的结果一定是a
。 得到a
以后,然后通过a^(a^b) = b
,可以得到b
的值。
问题三完整代码
public class LeetCode_0260_SingleNumberIII {
public static int[] singleNumber(int[] arr) {
int eor = 0;
for (int n : arr) {
eor ^= n;
}
// 假设出现奇数次的两种数为 a和b
// eor = a ^ b
// 获取最右侧的1
int a = 0;
int rightOne = eor & ((~eor) + 1);
for (int n : arr) {
if ((n & rightOne) == 0) {
a ^= n;
}
}
int b = a ^ eor;
return new int[]{a, b};
}
}
当有如下公式计算一个数最右侧的1
以后
x & ((~x) + 1)
我们还可以解决如下问题:
思路:即提取出最右侧的1
以后,与目标数进行与运算(&), 得到一个新的目标数,然后继续提取新目标数的最右侧的1
,如此往复,即可把所有位置的1
都提取出来。
问题四 一个数组中有一种数出现k次,其他数都出现了m次,m > 1, k < m, 找到出现了k次的数
要求:假设数组中所有数都是int类型,额外空间复杂度O(1),时间复杂度O(N)
问题四解题思路
我们可以这样考虑,设置一个32位的数组,
int[] help = new int[32];
遍历原始数组中每个数num的每一个二进制位, 伪代码如下:
for (int num : arr) {
for (int i = 0; i < 32; i++) {
help[i] += num的二进制中i位置的值(只能是0或者1)
}
}
经过以上循环,help
数组就把数组中的所有数的二进制位上的信息累加起来了。
help[0]
表示数组中所有数二进制中0位置的值之和;
help[1]
表示数组中所有数二进制中1位置的值之和;
......
help[31]
表示数组中所有数二进制中31位置的值之和。
然后i
从0
位置开始拿出help[i]
的值,假设help[i]=x
,用x % m
, 如果结果是k
,说明出现k
次的元素在这个位置上是1
, 否则,这个出现了k
次的数在i
位置上是0
, 遍历完help
数组,出现k
次元素的每一位信息都拿到了,然后还原出来即可。
问题四完整代码
public static int km(int[] arr, int k, int m) {
int[] helper = new int[32];
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < 32; j++) {
helper[j] += ((arr[i] >> j) & 1);
}
}
int ans = 0;
for (int i = 0; i < 32; i++) {
if (helper[i] % m == k) {
ans |= (1 << i);
}
}
return ans;
}