类似我的上一条博文,利用位运算求出数组中只出现一次的数
这道题很明显运用位运算来求解效率才是最高的。
但是出现了一个问题,现在只出现一次的数有两个,如果还是将所有数进行异或操作,
那么最终的结果是 a^b 。 但是我们现在要求的是 a 跟 b 分别为多少而不是 a^b.。那么要怎么做呢?
首先我们要明确,进行异或操作的做法是正确,那么怎样才能求出来呢?如果是只有一个数出现一次,
其他数出现两次那就很容易了但现在是两个数 只 出现一次。那么我们是不是可以把 这个数组分为两部分
A 部分 和 B 部分,并且使得 a 在 A 中而 b 在 B中,且其他数在 A 或者B中要出现两次,而不是一个在A
中一个在 B中。这样我们就可以分别对 A 、B中的元素进行 异或,得到的结果就是我们要求的 a 和 b 。
想法有了 ,but how to do ?如何将数组分成符合我们需求的 A 和 B呢?
我们是要从 a ^ b入手,先码具体代码:
/**
* <p>给定一个整型数组,数组里面只有两个元素出现一次,其他元素都出现两次</p>
* <p>很明显要运用位运算的知识</p>
* @deprecated num1 和 num2 分别用于存储我们要求的那两个数
*
* @author luzi
*
*/
public class FindNumsAppearOnce {
public void findNumsAppearOnce(int[] arr,int[] num1,int[] num2){
int temp = 0;
int indexOf1 = 1;
for(int i = 0;i < arr.length;i++){
temp^=arr[i]; //将所有的元素进行异或运算,则最后的结果是两个不同的元素的异或结果
}
while((temp&1) == 0 && indexOf1 < 32){ //这一步是关键,找出 temp 中第一个为 1 的位,据此将数组分为我们需要的A 和 B
indexOf1++;
temp=temp>>1;
}
temp = 1<<(indexOf1 - 1);
for(int i = 0;i < arr.length;i++){
if((arr[i]&temp) == 0)
num1[0] ^= arr[i];
else
num2[0] ^= arr[i];
}
}
}
关键的一步:
while((temp&1) == 0 && indexOf1 < 32){
indexOf1++;
temp=temp>>1;
}
因为a 和 b 不同,那么它们异或的结果 temp 至少有一位为 1 ,我们现在要做的就是找出 temp 第一个 1 哪一位。
将 temp 和 1 进行与运算,如果 temp 第一位 为 1(当然是指temp 右边的第一位也就是 低位)那么 temp & 1 = 1,
如果不是 那么 temp & 1 = 0 。我们现在进行一个 while 循环,当 temp & 1 = 0 时证明temp 第一位不是 1 ,我们将
temp 进行右移,同时将 indexOf1++ (用于记录当前比较的在哪一位)。当找到 第一位 1 时 while 循环停止,我们现在
得到 第一位 为 1 的位置 indexOf1. 。得到这个有什么用?得到这个我们就能将 数组分为我们要的 A 和 B 了!我们知道,
进行异或操作后得到的 temp 出现的 1 对应的那一位,必定是 在 a 、b中的某一个对应位是 1,而不是两个对应位都是 1,
如果都是 1 (或者都是 0)那么异或后对应位的结果就是 0 了。比如 0010 ^ 1011 = 1001. ,我们得到第 indexOf1 = 0位为 1
此时我们可以用 1 作为 比较标准,与 1 异或结果为 1 的放进 A中,与 1 异或结果 为 0 放进 B中,此时 A 和 B就是我们要求的。
如果indexOf = k ,那么我们就需要以 1<<k-1 作为比较的标准了,为什么?这个其实很好理解,只有懂位运算就能理解了。
这里就不赘述了。接下来我们就进行实际的分类来分为 A 和 B:代码比较简洁
temp = 1<<(indexOf1 - 1);
for(int i = 0;i < arr.length;i++){
if((arr[i]&temp) == 0)
num1[0] ^= arr[i];
else
num2[0] ^= arr[i];
}
到此,终于解决问题了!总结一下这题:位运算,以及将问题细化。即上面分析中将 数组分为 两个不同部分 A 和 B这一个方法,
非常重要,需细细体会。