据传说是MS/Google等等IT名企业的面试题:
有一组数字,从1到n,中减少了一个数,顺序也被打乱,放在一个n-1的数组里
请找出丢失的数字,最好能有程序,最好算法比较快
BTW1: 有很多种方法的哦,据说O(n)的方法就不止一种
BTW2: 扩展问题,如果丢失了2个数字呢?
BTW3: 一定要小心不要溢出,嗯,面试者有时候不会提醒你的
BTW4: 最好不要多申请n多空间
Update 一个很相近的题目:1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间,能否设计一个算法实现?
题目 :
- 给你n个数,其中有且仅有一个数出现了奇数次,其余的数都出现了偶数次。用线性时间常数空间找出出现了奇数次的那一个数。
- 给你n个数,其中有且仅有两个数出现了奇数次,其余的数都出现了偶数次。用线性时间常数空间找出出现了奇数次的那两个数。
现在这里给出一些解答:
第一题: 看到题目的第一反应就是把这个n-1个数字加起来,然后和1+2+3..+n的和进行比较,那个差值就是迷失的数字。但是这个方法正是BTW3里面提到的 不要溢出:)所以有一定的风险,还是不采用,还有人说用一个个+-来判断,可以是可以但是代码写起来也比较难看(个人感觉),还是异或操作符来的最合适一 些。 我们知道1^1=0;2^2=0;n^n=0;k^0=k;所以如果我们把这n-1个数字异或起来,再来异或一下1,2,..n。那么最终的答案肯定是迷 失数字k^(1^1)^(2^2)...^(n^n)。也就是K了。我们很容易地写下来了函数:
1 // in case find one missing number, here size is 1 less than the range n 2 int find_missing_number1 (int a[], int size) 3 { 4 int number=0; 5 for (int i=0;i<size;i++) 6 number ^= ((i+1)^a[i]); 7 number ^= (size+1); 8 return number; 9 }
第二个问题来的更复杂一些,如果有两个数字迷失怎么办?还是方法一的方法,但是需要衍生一下。假定我们迷失的数字是S1,S2那么我们全部异或之后 得到的就是S1^S2只有的值。分析一下就可以知道,S1!=S2,也就是说S1^S2!=0; 这样也就是说S1^S2的这个值有二进制位有一位是1,那么我们就可以把这些所有的数字分成2组,一组这个二进制位是1,另一个这个二进制位是0的来重新 做异或。这样就可以吧其中一个S1求出来了,那再S1^(S1^S2)一下,S2也就得到了。类似地,我们写下了如下的代码:
1 // in case find two missing numbers, here size is 2 less than the range n 2 void find_missing_number2 (int a[], int size, int& miss1, int& miss2) 3 { 4 miss1 = 0; 5 miss2 = 0; 6 int number=0; 7 for (int i=0;i<size;i++) 8 number ^= ((i+1)^a[i]); 9 number ^= (size+1); 10 number ^= (size+2); 11 12 // now number will be miss1^miss2 13 // find the binary 1 in number 14 int k = number - (number&(number-1)); 15 for (int i=0;i<size;i++) 16 { 17 if ( (i+1)&k ) 18 miss1 ^= (i+1); 19 if ( a[i]&k ) 20 miss1 ^= a[i]; 21 } 22 if ( (size+1) & k ) 23 miss1 ^= size+1; 24 if ( (size+2) & k ) 25 miss1 ^= size+2; 26 miss2 = number ^ miss1; 27 }
还找了个test case 测试了一把:看起来两个函数运行的结论是正确的。
1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 int a[] = { 1,2,4,5,9,7,8,10,3 }; 4 int b[] = { 1,2,4,5,9,7,6,11,3,10 }; 5 int s1,s2; 6 int k = find_missing_number1(a,sizeof(a)/sizeof(a[0])); 7 find_missing_number2(b,sizeof(b)/sizeof(b[0]),s1,s2); 8 std::cout<<"missing number 1 is "<<k<<std::endl; 9 std::cout<<"missing number 2 is "<<s1 << " and "<<s2<<std::endl; 10 system("pause"); 11 return 0; 12 }
后面的问题,Update确实如上所说的,非常类似,把这些个数字和1-1000异或就得到了答案了。奇数偶数的其实问题的第一二小题分别和迷失一个数字和两个数字对应,想法完全一致,这里不做展开了。
--------------------------------
问题1:
1-N的自然数中,少了一个,找出这个数
O(n)空间的比较简单,下面给出几个个只需要1,2个额外变量的算法
(1)求和-容易溢出
S1=1+2+...+N=(N+1)N/2
然后遍历数列每次从S1中减去当前的数字
最后剩下的数字就是所求
为了防止溢出我们可以每次在S1大于一定的数字后,就去减,然后继续求和,再大于就继续减,以此类推。
(2)异或
Y1=1^2^3...^N
然后遍历数列每次异或当前的数字
最后剩下的就是要求的数字
实际上平时我们用的比较多的互逆运算有 加/减 乘/除 对数/幂数
往往忽略了异或也是有一定的互逆性的
(3)O(N)时间的移动-排序
将a[i]移动到a[a[i]],使得数组有序
然后找出空着的位置
(4)O(NlogN)时间的移动-排序
用快排的思想,在1-N中选取游标X对数组快排一次,如果X被放在a[X-1]的位置上那么,要找的数字在X-N之间
否则X被放在a[X-2]的位置上 要找的数字在1-X-1之间 递归求解,直到找的要找的数字。
问题2:
1-N个自然数,少了两个,找出这两个数
(1)求和-容易溢出
S1=1+2+...+N=(N+1)N/2
S2=1^2+2^2+...+N^2=(N+1)(2N+1)N/6 (这里^是表示上标,即平方)
...
对于少了K个数的情况,如果K很少,我们可以找出K个和上面类似的函数,计算总体值,然后用解K元一次方程得到结果
但要注意函数的选择
(2)异或
按照上面同样的方法,求出最后的值P等于两个数的异或
确定P从低位到高位的第一个1是第i位
现在用快排的思想,将数列分成两个区间A和B
其中A中第i位是0,B中的第i位是1
然后调用问题1中的方法来分别求解A和B
(3)O(N)时间移动-排序
跟上面一样,实际上这种方法对于少了K个数的情况都能适用。
(4)O(NlogN)时间移动-排序
跟上面的方法一样
如果X被放在a[X-1]位置上,要找的两个数字在X-N之间
如果X被放在a[X-2]位置上,要找的数字一个在1-X-1间,一个在X-N之间
如果X被放在a[X-3]位置上,要找的数字都在1-X-1间
对于少了K个数字的情况,这种方法也可以做,但实现起来就比较复杂了
问题3:
给你n个数,其中有且仅有一个数出现了奇数次,其余的数都出现了偶数次。用线性时间常数空间找出出现了奇数次的那一个数。
(1)异或
经过上面的介绍,应该想到异或
一个数跟自己偶数次异或是0
奇数次异或是自己
问题4:
给你n个数,其中有且仅有两个数出现了奇数次,其余的数都出现了偶数次。用线性时间常数空间找出出现了奇数次的那两个数。
看看问题2和3中用异或的方法,应该知道答案了吧?