面试题 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次的解法解决即可。

posted on 2014-03-02 11:41  Felix Fang  阅读(223)  评论(0)    收藏  举报

导航