面试题 40(*),数组中只出现一次的数字 (附带GNENERAL解法:所有数字出现a次,另两个数各出现b和c次)(数字XOR本身会成为0,学会利用这个特性)

想了20多分钟都没有良好的思路。看答案后,这可能是本书到现在为止最难的一题。
先将问题简化:如果数组中只有一个数字只出现了一次,其余都出现两次,求唯一那个只出现一次的数字。
出现两次的数字,我们肯定要用到什么方法让他们相互抵消掉。
请不要忘掉XOR是最好的方法。
因为0 XOR A = A,A XOR A = 0
令 value = 0,将value挨个和数组元素 XOR,结果就是那个唯一出现的数。(如此流畅而完美的解法!)
那么,难度升级:出现一次的数有两个,怎么解?
还是 value 挨个XOR,得到的数肯定不是0,否则就所有的数都出现了两次了。对于这个不是0的结果,取其的某个非0位Y,这个非0位意味着什么?
意味着那两个只出现一次的数在Y位上不同。
接着,将整个数组分为两部分,一部分在Y位上是0,另一部分在Y位上是1。对于出现两次的数,相同的数肯定在同一个部分里。
好,这样就把整个数组拆成了两部分,而且每部分都只有一个只出现一次的数。(如此变态而完美的解法!)
自己原来写的米有书上简洁,是先用两个指针互换的方式将Y位为0的放到了前面,Y位为1放到了后段,然后挨个处理各部分。
世界上,不需要移动数组元素,每次判断出Y位是0还是1就可以直接进行应有的计算。从而免去了打乱数组顺序的麻烦。
修改后的代码主体部分和书上基本一致,剩余部分比书上简单些。
#include <stdio.h> void FindTwoUnique(int* list, int size, int* num1, int* num2){ if(NULL == list) return; int value = 0; for(int i = 0; i < size; value ^= list[i++]); if(!value) return; int mask = 1; for(; mask ^ value; mask = mask << 1); *num1 = *num2 = 0; for(int i = 0; i < size; i++){ if(list[i] & mask) *num1 ^= list[i]; else *num2 ^= list[i]; } } // ====================测试代码==================== void Test(char* testName, int data[], int length, int expected1, int expected2) { if(testName != NULL) printf("%s begins: ", testName); int result1, result2; FindTwoUnique(data, length, &result1, &result2); if((expected1 == result1 && expected2 == result2) || (expected2 == result1 && expected1 == result2)) printf("Passed.\n\n"); else printf("Failed.\n\n"); } void Test1() { int data[] = {2, 4, 3, 6, 3, 2, 5, 5}; Test("Test1", data, sizeof(data) / sizeof(int), 4, 6); } void Test2() { int data[] = {4, 6}; Test("Test2", data, sizeof(data) / sizeof(int), 4, 6); } void Test3() { int data[] = {4, 6, 1, 1, 1, 1}; Test("Test3", data, sizeof(data) / sizeof(int), 4, 6); } int main() { Test1(); Test2(); Test3(); return 0; }
书上代码:
unsigned int FindFirstBitIs1(int num); bool IsBit1(int num, unsigned int indexBit); void FindNumsAppearOnce(int data[], int length, int* num1, int* num2) { if (data == NULL || length < 2) return; int resultExclusiveOR = 0; for (int i = 0; i < length; ++ i) resultExclusiveOR ^= data[i]; unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR); *num1 = *num2 = 0; for (int j = 0; j < length; ++ j) { if(IsBit1(data[j], indexOf1)) *num1 ^= data[j]; else *num2 ^= data[j]; } } // 找到num从右边数起第一个是1的位 unsigned int FindFirstBitIs1(int num) { int indexBit = 0; while (((num & 1) == 0) && (indexBit < 8 * sizeof(int))) { num = num >> 1; ++ indexBit; } return indexBit; } // 判断数字num的第indexBit位是不是1 bool IsBit1(int num, unsigned int indexBit) { num = num >> indexBit; return (num & 1); }
最后,我们把这道题上升到general 级别:一个数组中,所有其他数字出现a次,只有一个数字X出现b次,b%a!=0 ,求出这个出现b次的数。
对于这种题目,要学会把数字看成一个个散掉的bit位。
我们假设一个32位整数中,某一位,设这个位为q位。我们遍历整个数组,观察q位,我们可以发现,由于除X外所有数都出现了a次,那么如果我们统计下q位上1出现的次数,再把这个次数 mod a,得到的结果是什么?得到的结果要么为0,要么结果与 b % a相等。为0说明 X在q位上是0,结果为b % a就表示X在q位上是1。
因此,我们预设一个值value = 0,我们只要一边遍历数组,一边数数32位上每一位上1出现的次数,再把次数 mod a,如果不是0,就把value的对应位置1。value的值,就是X。
这样的算法,我们需要逐位统计,因此要循环32次。
代码如下,参数n表示数组大小,a表示其他数字出现的次数,b不需要参与计算:
int singleNumber(int A[], int n, int a) { int mask = 1; int count = 0; int value = 0; for(int i = 0; i < 32; i++){ for(int j = 0; j < n; j++){ if(mask & A[j]) count++; } value = value ^ (((count % a) ? 1 : 0) << i); mask = mask << 1; count = 0; } return value; }
leetcode上也有一道例题:Given an array of integers, every element appears three times except for one. Find that single one.
只需把上面的参数a赋值成3就可以了。
改进
这种算法的缺点在于逐位统计,需要循环32次。
由剑指Offer题目的启发,我们发现在适当情况下,可以用位运算来简化步骤,比如开篇的那一题,讨论的是a为2的时候的解法,用亦或运算的话,一次遍历直接搞定了。
为嘛可以这样做呢?
如果用general的思路去思考,a=2情况下的解法就是:遇到 %2 == 0,就置0;就是每一位上,累计1出现得次数为2时,就变回0,这不就是亦或运算么,所以在亦或运算下,一次循环就算出了每一位的最后值。
那么a = 3的情况呢?遇到%3 == 0,就置0,有没有依旧不额外循环,直接单次遍历搞定的方法呢?
结合上面解体思路,其实就需要通过位运算,来做到单次遍历就能完成 在每一位上,1累计出现次数为3次时,就置0。
当a=2时,我们定义了一个初值num=0,然后挨个亦或。但是这里,初值只有一个是不够的,因为单个数每一位只有0/1两种状态,无法表达第三次出现。那我们可以再引入一个数,用两个数num1和num2来参与计算,num1表示计算结果值,num2表示进位值。对于每一位,当num1和num2在此位上都为1时,就意味着这一位要归零了。我们就这样通过用两个二进制位表达三进制 的方法,解决了%3 问题。
当遇到数A[i]时,假设 P=num1 ^ A[i], Q = num1 & A[i],这两个式子是二进制加法的常用算法。此时对于每一位,当其在P和num2都出现了1,这一位就需要置回0。然后把Q加入到num中去。仔细想想就会明白Q和num的相同位上只最多只能有一个1,所以加法可以用 | 代替。
因此LeetCode上的题目也可以这么解:
class Solution { public: int singleNumber(int A[], int n) { if(n == 0) return 0; int num1 = 0, num2 = 0, Q = 0, doubleOne = 0; for(int i = 0; i < n; ++i){ Q = num1 & A[i]; num1 ^= A[i]; doubleOne = ~(num1 & num2); num1 &= doubleOne; num2 &= doubleOne; num2 += Q; //或者写成num2 |= Q } return num1; } };
稍稍变化:a = 3,b = 2
如果a = 3,b = 2呢?也就是说,题目成了 Given an array of integers, every element appears three times except for one appearing twice. Find that one.
因为a = 3没变,因此思路主干没变:依然 %3 == 0时,置为0。不同的在于,因为b = 2,因此 当1累计出现两次后,我们在最后结果的对应位上返回1。基于这个思路,最后就不能return num1了,因为num1是通过不断亦或 算出来的,亦或两次1 的话,返回的是 0。我们要的是通过 & 算出来的结果,这样1累计出现两次时,对应位依然是 1。这个数就是num2。
所以当b = 2时,上面的算法不需要改变,最后返回 num2即可,此时num1应该为0。b = 1的时候,最后返回num1时,num2是0。
增大难度,引入另一个数字Y
这次除了X次数不一样,还有个Y,它出现的次数是c次,而且c%a!=0,b和c之间也不互为倍数。
题目成了:
一个数组中,所有其他数字出现a次,只有数字X出现b次,还有Y出现了c次,c%a!=0,b%a!=0 ,b和c之间也不互为倍数,求出X和Y。
我们依旧使用上面的逐位统计1次数的方法,最后我们同样可以得到每一位上1出现的次数,然后,依然将这些次数都mod a。
我们可以想一下,mod a后,这32个数中,如果有数是0,表达什么?这表示X,Y在这个位上都是0
如果不为0,其值是什么呢?实际上,如果不是0,值只能是三种情况:b%a+c%a,b%a,c%a,很显然,对应三种X, Y组合在这个位上是否为1的情况。
那我们只要挑一个值为b%a或者c%a的位,为什么呢?这表示,在这一位上,X, Y一个是1,一个是0,然后基于这一位将数组划分为两个数组,新的数组中,我们就可以保证X,Y被分散到两个数组中去了。然后用上文中只有一个数字出现b次的解法解决即可。
------------------------------------------------
Felix原创,转载请注明出处,感谢博客园!
posted on 2014-03-02 11:41 Felix Fang 阅读(223) 评论(0) 收藏 举报
浙公网安备 33010602011771号