从面试题中学算法(2)---求数组中唯一n个出现1次的数字(n=1,2,3)

       题目要求:给定一个整数数组,其中只有1个数出现1次,其他数字都出现偶数次。找出这个数字。如果其中只有2个数字出现1次呢?3个数字出现一次呢?依次找出。

   例子:数组a[] ={1,2,3,4,2,1,3} 那么程序输出 仅出现一次的数字为4。数组a[] = {1,2,3,2,3,4}.那么程序输出2个仅出现一次的数字为1,4. 数组a[]= {1,2,3,3,2,4,6}那么程序输出 3个仅出现一次的数为1,4,6.

好了,题目理解清楚了,下面就考虑解法了。首先,说明一个位运算---异或。这个对解题非常重要。异或是位运算的一种,指的是两个数字的二进制,每个位逐一异或。两个位相同则该位为0,不同则为1。异嘛,肯定不同为1.比如:3 ^ 4 = (011)^(100) =(111)。并且异或满足交换律。这个交换律很重要。x=a^b^c=a^c^b。0和x异或都为x。

下面说明3个非常重要的等式。

1. -n = (~n+1) = ~(n-1)   

        一个数取负数运算将等于该数按位取反加1,并且也等于该值减1取反。从补码角度理解,第二个式子恰好为负数的补码(负数的补码为源码取反加1)对应于n而言,其负数就为-n嘛。

2.保留一个数字的最右边为1的位,其他位都置为0.   n = n &(~n+1 )

比如n为十进制 7,对应二进制为0111.那么保留该数字的最右边1为: n = n &(~n+1 )=0001。可见只保留了最右边位为1的位。

3.将一个数的最右边1变为0,其余的位不变。 n = n &(n-1) .比如n为 十进制7,对应二进制为0111.n = n &(n-1) =0110。仅将最右边为1的位变为0。

一.找出1个仅出现一次的数字

在数组中,只有一个数字出现一次,其他数字都成对出现,那么如果对整个数组进行异或操作呢?由于异或操作满足交换律,而两个相同的数字之间异或为0,而0和任意数异或都为任意数。所以遍历一遍数组,且依次将结果异或,最后的异或值就是所查找的数字。比如a[] ={1,2,3,4,2,1,3}  xorRst 为异或值。将数组中所有元素异或下。xorRst =1^2^3^4^2^1^3。根据交换律xorRst =(1^1)^(2^2)^(3^3)^4其中括号内的值都为0.所以xorRst =0^0^0^4=4.即是结果。代码如下:

int FindSingleNum(int a[],int len)
{	
	int Rst = 0;
	if(a != NULL && len > 0)
	{
		for(int i=0 ; i<len; ++i)
			Rst ^= a[i];
	}
	return Rst ;
}

二.找出2个仅出现一次的数字

       那么问题升级为数组中有2个仅出现一次的数字。既然我们有了上面的分析,那么2个数字的能不能按照上面思路走呢?如果我们找到了区分这个2个数字的规则,就可以将该2个数字分到2组中去,而对于每个组而言,都可以按照出现1个数字的方法解决。那么问题就解决了。那现在是如何区分这个两个数字呢?我们还是将所有元素进行异或后结果。xorRst = a^b。(a,b为仅出现1次的2个数,其他成对的数都异或为0了,见前面分析)。由于a,b不同,所以a,b的对应的每个位至少有一个位不同,我们如果找出这个了位,那么a,b就可以依次分入到两个不同的组中去。而xorRst的最右边为1的数(如何求就是前面介绍的3个等式之一)。就完全表示。假设xorRst最右边为1的位是第m位。则a,b的第m位肯定不同。依次来将数组元素进行分组,相同的数字肯定是会分入到同一组中(因为相同的数每位都相同)。

int LastBitof1(int num)
{
	return num & (~num+1) ;
}
bool FindTwoOnceNum(int a[],int len,int &first,int &second)
{	
	bool RstState = false ;
	if(a != NULL && len > 0)
	{	
		int xorRst = 0 ;
		for(int i= 0 ; i < len ; ++i)
			xorRst ^= a[i] ;
		xorRst = xorRst & (~xorRst +1 );//get last bit of 1
		first = second = 0 ;
		for(int j=0 ;j<len ;++j)
		{
			if(a[j] & xorRst ) //通过xorRst 来分组
				first ^= a[j];
			else
				second ^= a[j];
		}
		RstState = true ; 
	}
	return RstState ;
}

三.找出3个仅出现一次的数字

那么问题如果在升级,一个数组中有3个仅出现一次的数字呢?有了前面两个问题的思路,我们猜想,也就是该如何进行分组?将a,b,c三个数如果分入到2个组中,分别对两组求解异或值,其中一组的异或结果就是a,b,c中的一个,有了这个,那么问题就回到第二个问题了。也就解决了。那么关键是如何进行a,b,c的分组呢?如何找到一个却别a,b,c的方法呢?

依然继续将数组中所有的元素进行异或求值。其结果为 xorRst = a^b^c(a,b,c为三个仅出现一次的数)。

下面将开启一连串的逻辑推导。

1.由于a,b,c三个数互不相等。所以xorRst与a,b,c都不相等。

证明:假设xorRst与a相等。 则  a = a^b^c.那么 b^c=0 ,即是 b等于c,与a,b,c互不相等矛盾。

2.因为xorRst与a,b,c都不相等。所以xorRst^a,xorRst^b,xorRst^c都不为0.(两个不同数异或结果肯定不为0)

3.假设有这样一个函数f(n)=lastBifof1(n)(问题2代码中的一个函数),f(n)函数目的就是保留数n的最右边1位,其他位都置为0。这是前面提到的3个等式中的一个。所以看下:这样的结果

f(xorRst ^a ) ^f(xorRst ^b ) ^f(xorRst ^c )  。这个式子意思就是,分别求xorRst^a,xorRst^b,xorRst^c最右边为1的位。然后将结果异或起来。从二进制角度看,其实也就是 三个只有一个位为1的数进行异或。因为f(xorRst ^a ) , f(xorRst ^b ) ,f(xorRst ^c ) 三个数都不为0.所以f(xorRst ^a ) ^ f(xorRst ^b ) ^f(xorRst ^c ) 结果也不为0(如果该结果为0,那么至少有一个f(xorRst^n)(n=a,b,c)为0,与前面f(xorRst^a,b,c)不为0矛盾。

所以该条结论:A= f(xorRst ^a ) ^ f(xorRst ^b ) ^f(xorRst ^c ) 不为0. 即是至少有一位为1.

4. 假设A的第m位为1,那么得出的结论就是 xorRst ^a,xorRst^b,xorRst^c 这三个数中仅有一个数的第m位为1或者三个数的第m位都为1.(这是很好得出的 1=1^1^1或者 1=0^1^0 就这两种情况)。

下面证明:xorRst ^a,xorRst^b,xorRst^c 三个数的第m位都为1的情况是不可能的。

证明:假设xorRst^a,xorRst^b,xorRst^c 第m位都为1.那么xorRst 的第m位都与a,b,c的第m位相反(位不同才为1嘛)。那么也就是a,b,c的第m位都相同。

4.1 假设a,b,c的第m位都为0.因为xorRst与a,b,c在第m位都相反,所以 xorRst 的第m位为1.有因为xorRst=a^b^c。所以在xorRst在第m位上是为0的。与前面矛盾。

4.2 假设a,b,c的第m位都为1.因为xorRst与a,b,c在第m位都相反,所以 xorRst 的第m位为0,又因为xorRst=a^b^c 所以xorRst的第m位为1.与前面的矛盾。

5.结论:xorRst ^a,xorRst^b,xorRst^c 这三个数中仅有一个数的第m位为1。由此找到了区分a,b,c三者的方法。那么依据xorRst ^n的第m位来分组a,b,c三个数进入到两个不同组中去。

void Swap(int &a,int &b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
bool FindTreeOnceNum(int a[],int len, int &first,int &second, int &third)
{
	bool RstState = false ;
	if(a != NULL && len > 0)
	{
		
		int xorRst = 0,i;//异或结果
		for(i=0; i<len; ++i)
			xorRst ^= a[i];
		//下面求 f(x^a)^f(x^b)^f(x^c)的最右边为1的第m位
		int  flag =0;
		for(i=0 ; i<len ; ++i)
			flag ^= LastBitof1(xorRst ^ a[i]);
		flag = LastBitof1(flag); //只保留最右边为1的位
		//求first
		first = second = third = 0 ;
		vector<int> vec;
		for(i=0 ; i<len ;++i)
			if(LastBitof1( xorRst ^ a[i] ) == flag )//x^a的第m位为1的情况 这里的m位其实是最右边为1位
				first ^= a[i];  //这样first 就求出了
		for(i=0 ; i<len ;++i)
			if( a[i] == first)
				Swap(a[i],a[len-1]);//移除该first值
		FindTwoOnceNum(a,len-1,second,third);
		RstState = true; 
	}
	return RstState ;
}

四.完整代码及其测试

#include <iostream>
#include <vector>
using namespace std;

int FindSingleNum(int a[],int len)
{	
	int Rst = 0;
	if(a != NULL && len > 0)
	{
		for(int i=0 ; i<len; ++i)
			Rst ^= a[i];
	}
	return Rst ;
}

int LastBitof1(int num)
{
	return num & (~num+1) ;
}
bool FindTwoOnceNum(int a[],int len,int &first,int &second)
{	
	bool RstState = false ;
	if(a != NULL && len > 0)
	{	
		int xorRst = 0 ;
		for(int i= 0 ; i < len ; ++i)
			xorRst ^= a[i] ;
		xorRst = xorRst & (~xorRst +1 );//get last bit of 1
		first = second = 0 ;
		for(int j=0 ;j<len ;++j)
		{
			if(a[j] & xorRst ) //通过xorRst 来分组
				first ^= a[j];
			else
				second ^= a[j];
		}
		RstState = true ; 
	}
	return RstState ;
}
void Swap(int &a,int &b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
bool FindTreeOnceNum(int a[],int len, int &first,int &second, int &third)
{
	bool RstState = false ;
	if(a != NULL && len > 0)
	{
		
		int xorRst = 0,i;//异或结果
		for(i=0; i<len; ++i)
			xorRst ^= a[i];
		//下面求 f(x^a)^f(x^b)^f(x^c)的最右边为1的第m位
		int  flag =0;
		for(i=0 ; i<len ; ++i)
			flag ^= LastBitof1(xorRst ^ a[i]);
		flag = LastBitof1(flag); //只保留最右边为1的位
		//求first
		first = second = third = 0 ;
		vector<int> vec;
		for(i=0 ; i<len ;++i)
			if(LastBitof1( xorRst ^ a[i] ) == flag )//x^a的第m位为1的情况 这里的m位其实是最右边为1位
				first ^= a[i];  //这样first 就求出了
		for(i=0 ; i<len ;++i)
			if( a[i] == first)
				Swap(a[i],a[len-1]);//移除该first值
		FindTwoOnceNum(a,len-1,second,third);
		RstState = true; 
	}
	return RstState ;
}
void printArray(int a[],int len)
{
	if( a != NULL && len > 0)
	{
		for(int i=0 ; i<len; ++i)
			cout<<a[i]<<" ";
		cout<<"\n";
	}
}
void Test1()
{	
	int a[] = {1,2,3,4,3,2,1};
	int len = sizeof(a)/sizeof(int);
	cout<<"数组中1个出现1次的数:\n";
	cout<<"数组元素为:\n";
	printArray(a,len);
	cout<<"所求值为: "<<FindSingleNum(a,len)<<endl;
	cout<<"---------------------------\n";
}
void Test2()
{	
	int a[] = {1,2,3,4,3,2,1,9};
	int len = sizeof(a)/sizeof(int);
	cout<<"数组中2个出现1次的数:\n";
	cout<<"数组元素为:\n";
	printArray(a,len);
	int first ,second;
	FindTwoOnceNum(a,len,first,second);
	cout<<"所求值为: "<<first<<" "<<second<< endl;
	cout<<"---------------------------\n";
	
}
void Test3()
{	
	int a[] = {1,2,3,4,3,2,1,9,7};
	int len = sizeof(a)/sizeof(int);
	cout<<"数组中3个出现1次的数:\n";
	cout<<"数组元素为:\n";
	printArray(a,len);
	int first ,second,third;
	FindTreeOnceNum(a,len,first,second,third);
	cout<<"所求值为: "<<first<<" "<<second<<" " <<third<< endl;
}
int main()
{
	Test1();
	Test2();
	Test3();
	return 0 ;
}

六.参考资料

http://zhedahht.blog.163.com/blog/static/25411174201283084246412/何海涛的博客 剑指offer作者


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值