题目:在数组中,有一个数字只出现了一次,而其他数字全都出现了N次(N>1),用O(N)的时间复杂度和O(1)的空间复杂度找到那个数字。
1. N = 2
比较经典的题目了,利用a ^ a = 0且异或操作满足交换律、结合律的特性,把数组里所有数字都做一次异或操作,相同的数字会被抵消掉,最后得到的就是只出现一次的那个数字了。
public int findUnique(int[] nums) {
int result = 0;
for (int i = 0; i < nums.length; i++) {
result = result ^ nums[i];
}
return result;
}
变形:N = 2,但只出现一次的数字有两个。
延续上一题的思路,假设数字内容为[a, b, c, c, d, d],那么在对数组里所有数字进行异或之后,得到的结果(假设k)就是我们要找的那两个数字的异或值:k = a ^ b。因为a不等于b,所以k必不为0。
也就是说,k中至少有一位是1,假设是第i位好了,这个第i位的1要么属于a,要么属于b。依据这个规则,可以把数组里的数字也分为两类:①第i位为1的数以及②第i位为0的数,假设a的第i位为1,那么a归属于①类,b归属于②类。
此时,a就是第①类中唯一一个只出现了一次的数字,根据上一题的解法,只要把数组中第i位为1的数字全部做一次异或,就能得到a了。同理,把数组中第i位为0的数字全部做一次异或,就能得到b,但不必这么麻烦,得到a后,只需再做一次异或即可:b = k ^ a。
// 用num1和num2记录结果
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
// 这里的two就是要找的两个数字的异或值
int two = 0;
for (int i = 0; i < array.length; i++) {
two = two ^ array[i];
}
// flag是用来判断第i位是否为0的标志
// 通过移动1的位置并与two做与操作来确定flag
int flag = 1;
while ((two & flag) == 0 && flag > 0) {
flag = flag << 1;
}
num1[0] = 0;
num2[0] = 0;
for (int i = 0; i < array.length; i++) {
if ((array[i] & flag) == 0) {
num1[0] = num1[0] ^ array[i];
} else {
// num2[0]可以选择在得到num1[0]后与two做异或得到
num2[0] = num2[0] ^ array[i];
}
}
}
2. N > 2
此时,异或的方法不再有用。考虑到除了目标数字a外,其他数字都出现了N次,如果把这个数字去掉,那么二进制表示中每一位上的1都会出现N的倍数次。如果加上目标数字,那么如果哪一位上的1的出现次数不是N的整数倍,说明a在该位上是1,否则就是0。
// 此处n为其他数字出现的次数
public int findUnique(int[] nums, int n) {
// Java中的整型为32位
int[] count = new int[32];
// 统计各位上1出现的次数
// 为了方便,这里count[0]表示的是最低位(最右边),顺序与平常的二进制相反
for (int i = 0; i < nums.length; i++) {
int cur = nums[i];
int index = 0;
while (cur != 0) {
if ((cur & 1) != 0) {
count[index]++;
}
cur = cur >>> 1;
index++;
}
}
int result = 0;
// 因为count是从低位开始计的,要想从高位开始计算,需要从后往前遍历数组
for (int i = count.length - 1; i >= 0; i--) {
// 将上一个最低位编程倒数第二位,空出最右边的位置来
result = result << 1;
// 次数不为n的整数倍,说明该位是1,直接加1
if (count[i] % n != 0) {
result = result + 1;
}
}
return result;
}
该方法在n为2时也适用,但相比直接异或还是慢了不少。