題目:一個整型數組里除了兩個數字之外,其他的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。要求時間復雜度是O(n),空間復雜度是O(1)。
分析:這是一道很新穎的關於位運算的面試題。
首先我們考慮這個問題的一個簡單版本:一個數組里除了一個數字之外,其他的數字都出現了兩次。請寫程序找出這個只出現一次的數字。
這個題目的突破口在哪里?題目為什么要強調有一個數字出現一次,其他的出現兩次?我們想到了異或運算的性質:任何一個數字異或它自己都等於0。也就是說,如果我們從頭到尾依次異或數組中的每一個數字,那么最終的結果剛好是那個只出現依次的數字,因為那些出現兩次的數字全部在異或中抵消掉了。
有了上面簡單問題的解決方案之后,我們回到原始的問題。如果能夠把原數組分為兩個子數組。在每個子數組中,包含一個只出現一次的數字,而其他數字都出現兩次。如果能夠這樣拆分原數組,按照前面的辦法就是分別求出這兩個只出現一次的數字了。
我們還是從頭到尾依次異或數組中的每一個數字,那么最終得到的結果就是兩個只出現一次的數字的異或結果。因為其他數字都出現了兩次,在異或中全部抵消掉了。由於這兩個數字肯定不一樣,那么這個異或結果肯定不為0,也就是說在這個結果數字的二進制表示中至少就有一位為1。我們在結果數字中找到第一個為1的位的位置,記為第N位。現在我們以第N位是不是1為標准把原數組中的數字分成兩個子數組,第一個子數組中每個數字的第N位都為1,而第二個子數組的每個數字的第N位都為0。
現在我們已經把原數組分成了兩個子數組,每個子數組都包含一個只出現一次的數字,而其他數字都出現了兩次。因此到此為止,所有的問題我們都已經解決。
基於上述思路,我們不難寫出如下代碼:
/////////////////////////////////////////////////////////////////////// // Find two numbers which only appear once in an array // Input: data - an array contains two number appearing exactly once, // while others appearing exactly twice // length - the length of data // Output: num1 - the first number appearing once in data // num2 - the second number appearing once in data /////////////////////////////////////////////////////////////////////// void FindNumsAppearOnce(int data[], int length, int &num1, int &num2) { if (length < 2) return; // get num1 ^ num2 int resultExclusiveOR = 0; for (int i = 0; i < length; ++ i) resultExclusiveOR ^= data[i]; // get index of the first bit, which is 1 in resultExclusiveOR unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR); num1 = num2 = 0; for (int j = 0; j < length; ++ j) { // divide the numbers in data into two groups, // the indexOf1 bit of numbers in the first group is 1, // while in the second group is 0 if(IsBit1(data[j], indexOf1)) num1 ^= data[j]; else num2 ^= data[j]; } } /////////////////////////////////////////////////////////////////////// // Find the index of first bit which is 1 in num (assuming not 0) /////////////////////////////////////////////////////////////////////// unsigned int FindFirstBitIs1(int num) { int indexBit = 0; while (((num & 1) == 0) && (indexBit < 32)) { num = num >> 1; ++ indexBit; } return indexBit; } /////////////////////////////////////////////////////////////////////// // Is the indexBit bit of num 1? /////////////////////////////////////////////////////////////////////// bool IsBit1(int num, unsigned int indexBit) { num = num >> indexBit; return (num & 1); }
示例:
01 10 11 11 100 100 異或結果:11
分組:
01 11 11 異或num1=01
10 100 100 異或num2=10.
成功找到num1和num2.
求最低位1:
int get_first_bit(int num)
{
return num&~(num - 1);
}
求一個數最低位1的個數還有多種方法(編程之美提到過)。
參考:劍指offerhttp://zhedahht.blog.163.com/blog/static/2541117420071128950682/
相似題:
題目為:給你1-1000個連續自然數,然后從中隨機去掉兩個,再打亂順序,要求只遍歷一次,求出被去掉的兩個數。
(基本跟上面的題一樣)
解法1:使用異或。
說說異或的兩個特性:順序無關 / 對一個數異或兩次等於沒有異或。順序無關就是說異或的元素可以隨意交換順序,而不會影響結果。異或兩次可以理解為+x和-x。
首先,這兩個數組(打亂前和打亂后)各自異或,也就是1^2^…^1000,得到兩個異或值。再對這兩個異或值進行一次異或,這樣就得到了x^y的指(重復部分互相抵消了)。
獲取計算出的異或值的1所在的位置,並繼續異或
因為x和y是兩個不同的整數,所以這兩個數的異或結果,轉化為二進制的話,一定在某位是1,假設在第3位。也就是說如果把原始數組按第3位是否為0進行划分,就可以分成兩個數組,每個數組各包含一個被抽取的數。如果打亂后的數組也按這個規則划分為兩個數組,這樣就得到了4個數組,其中兩組是第3位為0,另外兩組是第3位為1。把第3位為0的兩個數組所有元素進行異或就能得到被抽取的一個數,同理也就能獲得另外一個被抽取的數,於是問題解決。
舉例:4個數: 01 10 11 100
我們假設去掉01和10.異或結果為11.按第0為是否為0.
01 11 10 100
11 100
把左邊的異或得到01
右邊的得到10,問題解決。
另一種方法:用方程求解。
m = ( 1 + 2 + ...+ 1000) - (998 個的和) x + y
n = ( 1 * 2 * .... * 1000) / ( 998 個的積)x * y
經公式計算:
x = sqart( pow( m , 2 ) / 4 - n ) + m /2
y = m - x
代碼測試:
double x = 3 ; double y = 39 ; double m = x + y ; double n = x * y ; x = Math.sqrt( m * m / 4d - n ) + m / 2 ; y = m - x ; System.out.println( x ); System.out.println( y );
另一道相似的題:
找數字分析
原題
數組A中,除了某一個數字x之外,其他數字都出現了三次,而x出現了一次。請給出最快的方法,找到x。
分析
乍一看這個題目,不少同學立馬給出了答案:異或。但舉個例子,就會發現,異或是行不通的,一般的方法是利用異或的的如下特性:
-
A xor A = 0
-
A xor 0 = A
但是這個題目中,數字都是奇數個的,直接采用之前類似題目的異或方法,已經不合適了。
除此之外,我們還可能想到如下的方法:
-
采用hashmap,時間復雜度O(n),空間復雜度O(n)
-
對數組A進行排序,然后在遍歷一次,時間復雜度O(nlogn),空間復雜度O(1) 這個方法還可以。
是否還有一些效果更好的方法呢?這一類的題目,即使簡單的異或不能解決,也可以從二進制位、位操作方面去考慮,總之這樣的大方向是不會錯的。
題目中,如果數組中的元素都是三個三個出現的,那么從二進制表示的角度,每個位上的1加起來,應該可以整除3。如果有一個數x只出現一次,會是什么情況呢?
-
如果某個特定位上的1加起來,可以被3整除,說明對應x的那位是0,因為如果是1,不可能被3整除
-
如果某個特定位上的1加起來,不可以被3整除,說明對應x的那位是1
根據上面的描述,我們可以開辟一個大小為32的數組,第0個元素表示,A中所有元素的二進制表示的最低位的和,依次類推。最后,再轉換為十進制數即可。這里要說明的是,用一個大小為32的整數數組表示,同樣空間是O(1)的。
程序實現:
#include<iostream> using namespace std; void set(int& a,int i) { a |= (1<< (i & 0x1F));} void clr(int& a,int i) { a &= ~(1<<(i & 0x1f));} //除了某一個數字x之外,其他數字都出現了三次,而x出現了一次 void find(int a[],int n) { int m[32]; for(int i=0;i<32;i++) m[i]=0; for(int i=0;i<32;i++) { for(int j=0;j<n;j++) { int bit=a[j]&1;//&相當於mod 2 m[i]+=bit; a[j] >>=1; } } /* for(int i=0;i<32;i++) cout<<m[i]<<ends; cout<<endl; */ int result=0; for(int i=0;i<32;i++) { if(m[i]%3!=0) set(result,i); } cout<<"結果為"<<result<<endl; } int main() { int a[]={1,2,2,2,3,3,3}; int n=sizeof(a)/sizeof(a[0]); find(a,n); /* int b=2; set(b,0); clr(b,1); cout<<b<<endl; */ }
函數
void set(int& a,int i) { a |= (1<< (i & 0x1F));} 把a第i位置為1; void clr(int& a,int i) { a &= ~(1<<(i & 0x1f));} 把a的第i位清0.
參考了以前的位圖排序:http://www.cnblogs.com/youxin/p/3304667.html)
不過這里申請了一個數組的空間,如果這個是不被允許的呢?
參考:http://www.ituring.com.cn/article/56178
題目:一個數組中有三個數字a、b、c只出現一次,其他數字都出現了兩次。請找出三個只出現一次的數字。
(與最前面的一題不同,前面是2個不同,現在是3個)
(要求空間為O(1),所以用hash判斷是否重復這種方法不管用了)
分析:在博客http://zhedahht.blog.163.com/blog/static/2541117420071128950682/中我們討論了如何在一個數組中找出兩個只出現一次的數字。在這道題中,如果我們能夠找出一個只出現一次的數字,剩下兩個只出現一次的數字就很容易找出來了。
如果我們把數組中所有數字都異或起來,那最終的結果(記為x)就是a、b、c三個數字的異或結果(x=a^b^c)。其他出現了兩次的數字在異或運算中相互抵消了。
我們可以證明異或的結果x不可能是a、b、c三個互不相同的數字中的任何一個。我們用反證法證明。假設x等於a、b、c中的某一個。比如x等於a,也就是a=a^b^c。因此b^c等於0,即b等於c。這與a、b、c是三個互不相同的三個數相矛盾。
由於x與a、b、c都各不相同,因此x^a、x^b、x^c都不等於0。
我們定義一個函數f(n),它的結果是保留數字n的二進制表示中的最后一位1,而把其他所有位都變成0。比如十進制6表示成二進制是0110,因此f(6)的結果為2(二進制為0010)。f(x^a)、f(x^b)、f(x^c)的結果均不等於0。
接着我們考慮f(x^a)^f(x^b)^f(x^c)的結果。由於對於非0的n,f(n)的結果的二進制表示中只有一個數位是1,因此f(x^a)^f(x^b)^f(x^c)的結果肯定不為0。這是因為對於任意三個非零的數i、j、k,f(i)^f(j)的結果要么為0,要么結果的二進制結果中有兩個1。不管是那種情況,f(i)^f(j)都不可能等於f(k),因為f(k)不等於0,並且結果的二進制中只有一位是1。
於是f(x^a)^f(x^b)^f(x^c)的結果的二進制中至少有一位是1。假設最后一位是1的位是第m位。那么x^a、x^b、x^c的結果中,有一個或者三個數字的第m位是1。
接下來我們證明x^a、x^b、x^c的三個結果第m位不可能都是1。還是用反證法證明。如果x^a、x^b、x^c的第m位都是1,那么a、b、c三個數字的第m位和x的第m位都相反,因此a、b、c三個數字的第m位相同。如果a、b、c三個數字的第m位都是0,x=a^b^c結果的第m位是0。由於x和a兩個數字的第m位都是0,x^a結果的第m位應該是0。同理可以證明x^b、x^c第m位都是0。這與我們的假設矛盾。如果a、b、c三個數字的第m位都是1,x=a^b^c結果的第m位是1。由於x和a兩個數字的第m位都是1,x^a結果的第m位應該是0。同理可以證明x^b、x^c第m位都是0。這還是與我們的假設矛盾。
因此x^a、x^b、x^c三個數字中,只有一個數字的第m位是1。於是我們找到了能夠區分a、b、c三個數字的標准。這三個數字中,只有一個數字滿足這個標准,而另外兩個數字不滿足。一旦這個滿足標准數字找出來之后,另外兩個數字也就可以找出來了。
void getThreeUnique(vector<int>& numbers, vector<int>& unique) { if(numbers.size() < 3) return; int xorResult = 0; vector<int>::iterator iter = numbers.begin(); for(; iter != numbers.end(); ++iter) xorResult ^= *iter; int flags = 0; for(iter = numbers.begin(); iter != numbers.end(); ++iter) flags ^= lastBitOf1(xorResult ^ *iter); flags = lastBitOf1(flags); // get the first unique number int first = 0; for(iter = numbers.begin(); iter != numbers.end(); ++iter) { if(lastBitOf1(*iter ^ xorResult) == flags) first ^= *iter; } unique.push_back(first); // move the first unique number to the end of array for(iter = numbers.begin(); iter != numbers.end(); ++iter) { if(*iter == first) { swap(*iter, *(numbers.end() - 1)); break; } } // get the second and third unique numbers getTwoUnique(numbers.begin(), numbers.end() - 1, unique); } int lastBitOf1(int number) { return number & ~(number - 1); } void getTwoUnique(vector<int>::iterator begin, vector<int>::iterator end, vector<int>& unique) { int xorResult = 0; for(vector<int>::iterator iter = begin; iter != end; ++iter) xorResult ^= *iter; int diff = lastBitOf1(xorResult); int first = 0; int second = 0; for(vector<int>::iterator iter = begin; iter != end; ++iter) { if(diff & *iter) first ^= *iter; else second ^= *iter; } unique.push_back(first); unique.push_back(second); }
上文中getThreeUnique從數組中找出三個只出現一次的數字,而getTwoUnique從數組中找出兩個只出現一次的數字。lastBitOf1實現分析中的函數f(n)的功能,它只保留數字n的二進制表示中的最后一位1,而把其他所有位都變成0。
在函數getThreeUnique中,我們通過第一個for循環把a、b、c三個數字異或的結果保存到xorResult中,接着在第二個for循環中求出f(x^a)^f(x^b)^f(x^c)並保存到變量flags中。在語句flags=lastBitOf1(flags)求出f(x^a)^f(x^b)^f(x^c)結果的二進制中最后一位是1的位。並根據這一數位求出第一個只出現一次的數字first。接着把first交換到數組的最后,並在數組的前n-1個數字中求出另外兩個只出現一次的數字。
上面的簡單代碼版:http://blog.csdn.net/zzran/article/details/8108787
#include<stdio.h> int get_first_bit(int num) { return num&~(num-1); } void get_two_unique_num(int *a,int n,int *num1,int *num2) { int result_code=0; for(int i=0;i<n;i++) result_code^=a[i]; int diff=get_first_bit(result_code); *num1=0; *num2=0; for(i=0;i<n;i++) { if(a[i]&diff) { (*num1)^=a[i]; } else { (*num2)^=a[i]; } } } void get_three_unique_num(int *a,int n,int *num1,int *num2,int *num3) { int result_code=0; for(int i=0;i<n;i++) result_code^=a[i]; int flag=0; for(i=0;i<n;i++) flag^=get_first_bit(result_code^a[i]); flag=get_first_bit(flag); *num1=0; for(i=0;i<n;i++) { if(get_first_bit(result_code^a[i])==flag) { (*num1)^=a[i]; } } for(i=0;i<n;i++) { if(a[i]==(*num1)) { int temp=a[i]; a[i]=a[n-1]; a[n-1]=temp; break; } } get_two_unique_num(a,n-1,num2,num3); } void main() { int a[]={2,2,4,4,6,6,3,5,7}; int num1,num2,num3; get_three_unique_num(a,sizeof(a)/sizeof(int),&num1,&num2,&num3); printf("%d\t%d\t%d\n",num1,num2,num3); }
轉自:http://zhedahht.blog.163.com/blog/static/25411174201283084246412/