从一个数组中找出出现若干次的数字,这样的面试题目很多,通用的方法是使用Hash表,然后统计每个数字的出现次数,这种方法时间复杂度为O(n),空间复杂度也为O(n),但是对于几种特殊的情况却可以使用更高效的方法来解决。
一个整型数组里除了一个数字之外,其他的数字都出现了两次。如何找出数组中只出现一次的数字
可以使用上面说的Hash表方法,空间复杂读为O(n),但是如果要求时间复杂度是O(n),空间复杂度是O(1),那应该怎么解决呢?
由于除了一个数字外,其他每个数字都出现两次,而位运算中的异或运算对与两个相同的数字,他们的异或值为0,并且0与任何数字的异或为那个数字,所以可以使用这个规则来解决这个题目,即将数组中所有的数字求异或,那么最后得到的结果就必定是所求的数字。
变形
上面这题还可进行变形:有两个数组,一个数组中有100个元素,一个数组中有99个元素,有99个元素的数组中的数字互不相同,并且全部包含在有100个元素的数组中,求出这个不包含在99个元素数组中的数字。将这两个数组合并成一个数组就成了上面的那个题,但是根据具体情况,这道题还有其他的解法,如先分别对两个数组求算数和,100个元素的数组的算术和为m,99个元素的数组为n,那么m-n的值即为所有的数。此外如果这100个不同的数中没有零元素,且100个数的乘积不会溢出,那么可以先分别将这两个数组中所有元素相乘,再用大的乘积除以较小的乘积,得到的数字就是所求的数字。
求和和求乘积的方法有一个弊端就是在求和或者求积的过程中可能会产生溢出,如果溢出,最后的结果就不是理想的结果了,但是使用异或运算就可以很好的解决溢出的问题。
加大难度
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字
假设这两个只出现一次的数字分别是x,y,根据上面分析的思路,对这个数组中的所有元素求异或,那么最后得到的异或值为xor = x^y
但是这并不是不是所要求的结果,继续分析,由于x != y
,所有xor
肯定不为0,所以xor
的二进制位中肯定至少有一位为1,假设是第m位,那么x的第m位与y的第m位肯定不同,否则xor的第m为就为0了,假设x的第m位是1,y的第m位是0,根据这个条件,将原数组分为两组,一组为元素的第m位为1的整数组成的集合,一组为元素的第m位为0的整数组成的集合,那么就将原问题转换为了两个小问题,即一个整型数组里除了一个数字之外,其他的数字都出现了两次。如何找出数组中只出现一次的数字,问题解决。
实现代码如下:
/**
* 数组中有两个数出现一次,其余都出现两次,找出这两个数
* @param A
* @return
*/
public int[] twoSingleNumber(int A[]) {
int x, y, z;
x = 0;
for(int a : A) {
x ^= a;
}
int m = getFirstRightBit(x);
y = 0;
z = 0;
for(int a : A) {
if((m & a) == 0) {
y ^= a;
} else {
z ^= a;
}
}
return new int[]{y, z};
}
/**
* 取n的最右边的为1的位,如n=2,返回2,n=3,返回1,n=8,返回8
* @param n
* @return
*/
public int getFirstRightBit(int n) {
return n & ~(n-1);
}
上面的代码中,取xorResult的最右边的为1的位。
getFirstRightBit()
方法的作用就是取出数n的二进制表示的最右边比特位为1的数。
再加大难度
一个数组中有三个数字a、b、c只出现一次,其他数字都出现了两次。请找出三个只出现一次的数字
对于这个题目,先求出数组中所有数字的异或值xorResult,由于a与b不等所有,xorResult一定不等于0,找出xorResult中的一个为1的位,假设是第i位,xorResult的第i位为1,那么a与b的第i位肯定不同,所以接下来可以再第遍历数组,根据数组元素的第i位是1还是0将这个数组分为两组,再对每组算异或值,这样就求出来了这两个值。
如果数组中有三个数字a,b,c各出现一次,其他数字出现两次呢?根据博主何海涛的这篇博文分析http://zhedahht.blog.163.com/blog/static/25411174201283084246412/。首先对数组中的所有元素算异或值x,则x=a^b^c,所以,可以得到由于这是三个不同数字的异或值,所以不能像上面求两个不同的数字那样求解,但是如果先求出一个数,那么就可以将剩下的问题转化为与上面相同的问题。
可以定义一个函数f(n),它的功能与上面代码中的getFristRightBit()
函数一样。由于先求x^a,x^b,x^c这三个数不等于0,可以用反证法证明,所以f(x^a),f(x^b),f(x^c),这三个数也一定等于0,那么f(x^a)^f(x^b)^f(x^c)一定不为0,假设f(f(x^a)^f(x^b)^f(x^c))的二进制位从右到左的第m位为1,那么f(x^a),f(x^b),f(x^c)这三个数中必定只有一个的第m位为1,因为不可能存在有两个的第m为1,假设f(x^a),f(x^b),f(x^c)三个的第m位都是1,那么x^a,x^b,x^c的第m位都是1,则a,b,c这三个数的第m位与x相反,即a,b,c这三个数的第m位相同,要么都是0,要么都是1,但是x=a^b^c,三个相同的数异或还是那个数,矛盾。所以x^a,x^b,x^c中必定只有一个数的第m位为1。
根据上面的分析,就可以利用所以x^a,x^b,x^c中必定只有一个数的第m位为1这个条件来求出a,b,c中的一个数,余下的问题就转化为上面的求两个出现一次的数了。
实现代码如下:
/**
* 数组中有三个数字出现一次,其余都出现两次
* @param A
* @return
*/
public int[] threeSingleNumber(int A[]) {
int[] res = new int[3];//存放结果
//求出数组中所有元素的异或
int x = 0;
for(int tmp : A) {
x ^= tmp;
}
int flag = 0;
for(int tmp : A) {
flag ^= getFirstRightBit(x ^ tmp);
}
flag = getFirstRightBit(flag);//flag为f(f(x^a)^f(x^b)^f(x^c))
//找到第一个数字
int first = 0;//第一个求出的只出现一次的数字
int twoxor = 0;//余下两个数字的异或值
for(int tmp : A) {
if(getFirstRightBit(x ^ tmp) == flag) {
first ^= tmp;
} else {
twoxor ^= tmp;
}
}
res[0] = first;
int y = 0, z = 0;
int m = getFirstRightBit(twoxor);
//first的第m位要么是0,要么是1,所以可以根据这个条件,将first增加到数组中,
//这样就相当于first在数组中出现了两次
if((m & first) == 0) {
y = first;
} else {
z = first;
}
for(int tmp : A) {
if((m & tmp) == 0) {
y ^= tmp;
} else {
z ^= tmp;
}
}
res[1] = y;
res[2] = z;
return res;
}
上面的代码中与博文: http://zhedahht.blog.163.com/blog/static/25411174201283084246412/中实现的有一点不同,当求出第一个只出现一次的数字后,原博文中是遍历一次数组,将这个数字交换到最后,而上面代码的实现思路是既然求得了第一个只出现一次的数字first,那么就可以将first加入到数组中,相当于first在数组中出现了两次,但是并不是在数组中增加一个元素位置,而是对求得的剩下两个数字的异或值twoxor调用f函数,得到twoxor的最右边为1的位,假设是第m位,而first的第m位要么是0,要么是1,所以可以先对x或者y赋值位first,相当于将first加入到数组中运算了。
再次变形
一个全是32位整数的大数组,除了其中一个数字出现2次外,其余的数字都出现了3次。如何找出那个只出现了两次的数字?
网上给出的是下面这个算法,很巧妙,叹一下
def special(lst):
ones = 0
twos = 0
for x in lst:
twos |= ones & x
ones ^= x
not_threes = ~( ones & twos )
ones &= not_threes
twos &= not_threes
return ones
总体思想,一个整数有32位,对于这32个中的每一个位,将所有数字的这个位相加,在相加的过程中,如果这个位的和为3,则将这个位清零,那么最后得到的数字除以2就是所要找的数字。
上面的代码通过增加变量ones,twos和not_three作为迭代过程中的辅助变量,其中ones记录位所有出现了模3余1次的bit,twos记录所有出现了模3余2次的bit,not_three为ones & twos再取反表示当某一个bit出现第3k次(k是非负整数)时,就把这一位置为0,对于数组[1, 1, 1, 2],对于ones,twos,not_threes这三个整数,使用两位二进制表示,初始ones为00,twos位00,然后遍历数组,对于数组中第一个整数,twos为0,ones为01,对于数组中第二个整数,twos为01,ones为0,对于数组中的第三个整数,twos为01,ones为01,那么twos & ones为01,为1的为表示这一位出现了3次了,所以就应该将这位清0,即对twos & ones进行取反操作,那么not_three就为10了。
Reference
一个全是32位整数的大数组,除了其中一个数字出现2次外,其余的数字都出现了3次。如何找出那个只出现了两次的数字?:http://blog.csdn.net/erazy0/article/details/6443715
用位运算巧解元素出现次数问题:http://blog.csdn.net/dinosoft/article/details/6443354
程序员面试题精选100题(63)-数组中三个只出现一次的数字[算法] :http://zhedahht.blog.163.com/blog/static/25411174201283084246412/