面試題:找出數組中只出現一次的2個數(異或的巧妙應用)(出現3次)


題目:一個整型數組里除了兩個數字之外,其他的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。要求時間復雜度是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

 

題目:一個數組中有三個數字abc只出現一次,其他數字都出現了兩次。請找出三個只出現一次的數字。

(與最前面的一題不同,前面是2個不同,現在是3個)

(要求空間為O(1),所以用hash判斷是否重復這種方法不管用了)

分析:在博客http://zhedahht.blog.163.com/blog/static/2541117420071128950682/中我們討論了如何在一個數組中找出兩個只出現一次的數字。在這道題中,如果我們能夠找出一個只出現一次的數字,剩下兩個只出現一次的數字就很容易找出來了。

如果我們把數組中所有數字都異或起來,那最終的結果(記為x)就是abc三個數字的異或結果(x=a^b^c)。其他出現了兩次的數字在異或運算中相互抵消了。

我們可以證明異或的結果x不可能是abc三個互不相同的數字中的任何一個。我們用反證法證明。假設x等於abc中的某一個。比如x等於a,也就是a=a^b^c。因此b^c等於0,即b等於c。這與abc是三個互不相同的三個數相矛盾。

由於xabc都各不相同,因此x^ax^bx^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)的結果。由於對於非0nf(n)結果的二進制表示中只有一個數位是1,因此f(x^a)^f(x^b)^f(x^c)的結果肯定不為0這是因為對於任意三個非零的數ijkf(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^ax^bx^c的結果中,有一個或者三個數字的第m位是1

接下來我們證明x^ax^bx^c的三個結果第m位不可能都是1。還是用反證法證明。如果x^ax^bx^c的第m位都是1,那么abc三個數字的第m位和x的第m位都相反,因此abc三個數字的第m位相同。如果abc三個數字的第m位都是0x=a^b^c結果的第m位是0。由於xa兩個數字的第m位都是0x^a結果的第m位應該是0。同理可以證明x^bx^cm位都是0。這與我們的假設矛盾。如果abc三個數字的第m位都是1x=a^b^c結果的第m位是1。由於xa兩個數字的第m位都是1x^a結果的第m位應該是0。同理可以證明x^bx^cm位都是0。這還是與我們的假設矛盾。

因此x^ax^bx^c三個數字中,只有一個數字的第m位是1。於是我們找到了能夠區分abc三個數字的標准。這三個數字中,只有一個數字滿足這個標准,而另外兩個數字不滿足。一旦這個滿足標准數字找出來之后,另外兩個數字也就可以找出來了。

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循環把abc三個數字異或的結果保存到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

 

考慮給定數組中有三個單獨出現一次的數字,這個會比有兩個的稍微復雜。分步分析,設定這三個數為a,b,c:
(1)將數組中的數字全部異或,得到的結果x=a^b^c,但是x不是a,b,c中的其中一個,假設x=a,那么b^c=0說明b=c,與題目給定的條件矛盾。
(2)設定f(n)可以像2中的那樣,從低位開始,找到第一個bit為1的位置,f(x^a),f(x^b),f(x^c)得到的值肯定都不為0,因為x^a,x^b,x^c本身就不為0。f(x^a)^f(x^b)^f(x^c)結果不為0。因為f(x^a)^f(x^b)的結果中可能為0,也可能有兩個bit為1。如果假設f(x^c)的結果bit為1的位置與f(x^a)^f(x^b)的其中一個重合,則f(x^a)^f(x^b)^f(x^c)結果中只有1個bit為1,如果不重合的話那么有3個bit位為1。
(3)這便可以推斷出f(x^a)^f(x^b)^f(x^c)中至少有一個bit位為1。假設從低位到高位的第mbit位為1.那么可以得出結論x^a,x^b,x^c中有一個或者三個的第m位為1(不可能有兩個,因為有兩個的話,異或的結果就為0了)。
(4)證明,x^a,x^b,x^c中只有一個第m-bit位為1.假設他們的第m位都為1,那么x的第m位為0,但是x=a^b^c其第m位肯定為1,所以假設不成立。那么相反,假設x的第m位為1,a,b,c的第m位都為0,也不成立,因為x=a^b^c。所以綜上所述x^a,x^b,x^c中只有一個第m位為1。那么這個問題就好辦了。根據這個第m位找到第一個只出現一次的數字。然后剩下兩個就是問題2所描述的問題。下面給出代碼:
#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/

http://my.oschina.net/u/1455799/blog/262854


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM