落单的数II

问题描述

http://www.lintcode.com/zh-cn/problem/single-number-ii/

给出3*n + 1 个的数字,除其中一个数字之外其他每个数字均出现三次,找到这个数字。

样例
给出 [1,1,2,3,3,3,2,2,4,1] ,返回 4

笔记

代码1

利用字典,存下来不同数字出现的次数,最后把次数1次的返回。时间复杂度O(n),空间复杂度O(n)。

代码2

排序,然后再找只出现一次的数。快排时间复杂度O(nlogn)(每次partition是n,平均需要logn次),空间复杂度O(1)。

代码3

利用一个数组记录二进制每个位的和。

代码4

用到了神奇的技巧。用三个变量,模拟三进制加法不进位。因为如果实现了三进制加法不进位,可以做到把3个重复的数去除,最后只剩下“落单的数”。

  • one的某一位是1代表这一位是1,否则是0;
  • two的某一位是1代表这一位是2,否则是0;
  • mod是用来清零的,使用mod&one,mod&two,将one和two的某些位清0。
int one = 0;
int two = 0;
int mod = 0;

计算每一个数i的时候:

对于two

先判断有没有从one+i那边进位过来的。有进位的情况是one&i(对应的位都为1才会出现进位),将进位的情况与当前的two相加。有四种情况:

  • 原来two的这一位是0,无进位,相当于0+0=0;
  • 原来two的这一位是1,无进位,相当于2+0=2;这一位依然是1;
  • 原来two的这一位是0,有进位,相当于0+2=2;这一位依然是1;
  • 原来two的这一位是1,有进位,这是不可能的。有进位,说明原来one的对应位是1。现在,原来two的对应位也是1,我们早就应该把one和two进位清零了,不应该存在这种情况。

可见,这是一个或操作:two |= (one & i);

注意,two这一步用到了one,因此需要先算two,再算one,以防用了更新后的one。

对于one

使用异或。one ^= i; 即二进制加法不进位。

对于mod

如果one和two的某一位都是1,说明该进位清零了。使:
mod = ~(one & two)

如果one和two的某一位都是1,那么mod的这一位就是0,可以用这个0与one相与,将one的这一位清0:
one &= mod;
同理,对two也是一样
two &= mod;

除了上面那种情况,都是不需要进位的。这样mod的对应位就是1,相与之后保留one和two的相应位。

返回值

为啥是返回one呢?为啥不管two了?

因为题目中是只有一个数落单,其他都是三个三个的,在我们这个三进制加法不进位中全部抵消掉,结果是0,最后只会剩下那个落单的数,这个落单的数与0做”三进制加法不进位“,结果只会出现在one中。最后two必定为0。

举个例子把上面的流程跑一遍,加深理解。

[3,1,2]
初始化one=0,two=0,mod=0

加入3,0x11,two=0,one=0x11, mod=0x11, 不对one和two清零,one=0x11, two=0,看到one=0x11, two=0,已经把3纳入到我们的系统中了,结果是对的。

加入1,0x01。
对于two,two |= (one&1) 将1和one相与,0x01&0x11=0x01, 发现第0位进位了,因此要把这一位保存到two,two|=0x01, two = 0x01;
对于one,one ^= 1将1与one做异或,0x01^0x11=0x10。
对于mod,mod = ~(one&two),因为one(0x10)和two(0x01)没有对应位都是1的,因此不需要进位清零,mod=0x11。
最终one=0x10,two=0x01。结果也是正确的。(3+1=4,在三进制的表示为0x11,正好是one+two(1*2+2*1=4))

加入3,0x11。
对于two,two |= (one&0x11) 将3和one相与,0x11&0x10=0x10, 发现第1位进位了,因此要把这一位保存到two,two|=0x10, two = 0x11;
对于one,one ^= 0x11将3与one做异或,0x11^0x10=0x01。
对于mod,mod = ~(one&two),因为one(0x01)和two(0x11)的第0位都是1的,需要对第0位进位清零,mod=0x10。one&=mod,one=0x00,two&=mod, two=0x10
最终one=0x00,two=0x10。结果,(3+1+3=7,在三进制的表示为0x21,one=0x00,two=0x10,也就是0+1*4=4,已经进位清零了一次)

注:由于在我们的方法中,我们把三进制运算拆得更小了,所以在这种情况下,真正的三进制还没出现进位清零,但我们的方法已经出现了一次进位清零(7-3=4)

加入3,0x11。
对于two,two |= (one&0x11) 将3和one相与,0x11&0x00=0x00, 无进位,two|=0x00, two = 0x10;
对于one,one ^= 0x11将3与one做异或,0x00^0x11=0x11。
对于mod,mod = ~(one&two),因为one(0x11)和two(0x10)的第1位都是1,需要对第1位进位清零,mod=0x01。one&=mod,one=0x01,two&=mod, two=0x00
最终one=0x01,two=0x00。最后的结果是正确的。

小结

经过实验表明,这种方法模拟的三进制运算并不是真正的三进制运算,而是把真正的三进制加法拆的更细了,不能保证中间的结果与真正的三进制相同。但是恰巧我们题目中用到的三进制特性是三个相同的数相加,每一位最后都必定进位,正好用上了这个特性,真是太高超的走火入魔的技巧。花了好多时间去理解,还是感觉没有完全理解,而且这个方法是适用面太窄了,还是留给大神们玩弄这种魔法吧。。

代码

代码1

class Solution {
public:
    /**
     * @param A : An integer array
     * @return : An integer 
     */
    int singleNumberII(vector<int> &A) {
        // write your code here
        map<int, int> m;
        for (int i : A)
        {
            if (m.find(i) != m.end())
                m[i]++;
            else
                m[i] = 1;
        }
        for (auto i : m)
        {
            if (i.second == 1)
                return i.first;
        }

    }
};

代码2


class Solution {
public:
    /**
     * @param A : An integer array
     * @return : An integer 
     */
    int singleNumberII(vector<int> &A) {
        // write your code here
        sort(A.begin(), A.end());
        if(A[0] != A[1])
            return A[0];
        if(A[A.size()-1] != A[A.size()-2])
            return A[A.size()-1];
        for (int i = 1; i < A.size() - 1;i++)
        {
            if (A[i-1] != A[i] && A[i] != A[i+1])
                return A[i];
        }

    }
};

代码3

class Solution {
public:
    /**
     * @param A : An integer array
     * @return : An integer 
     */
    void set(int &a, int i)
    {
        a |= (1 << (i & 0x1F));
    }

    int singleNumberII(vector<int> &A) {
        // write your code here
        int bits[32];
        memset(bits, 0, sizeof(bits));
        for (int i = 0; i < 32; i++)
        {
            for (auto j : A)
            {
                bits[i] += ((j >> i) & 1);
            }
        }
        int res = 0;
        for (int i = 0; i < 32; i++)
        {
            if (bits[i] % 3 != 0)
                set(res, i);
        }
        return res;
    }
};

代码4

class Solution {
public:
    /**
     * @param A : An integer array
     * @return : An integer 
     */
    int singleNumberII(vector<int> &A) {
        // write your code here
        int one = 0;
        int two = 0;
        int mod = 0;
        for (auto i : A)
        {
            two |= (i & one);
            one ^= i;
            mod = ~(one & two);
            one &= mod;
            two &= mod;
        }
        return one;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值