據傳說是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中用異或的方法,應該知道答案了吧?