LeetCode_Single_Number_II

前言

此为leetcode中第137个问题Single Number II讨论中的一篇译文,这里是原文连接

1、问题描述

给定一个整数数组,一个元素出现p次,其余元素出现k (k > 1)次(p >= 1, p % k != 0)。找到出现p次的元素。


2、数组元素只有1位的特殊情况

正如其他人指出的,可以使用位操作,我们回顾一下整数在计算机中是如何表示的——按二进制位表示。首先,我们先来考虑只有一位的情况。假设我们有一个整数数组,它的元素只有1位二进制(0或者1),我们需要统计数组中1的数量,当1的数量达到某一个值,比如k时,计数重置为0,重新开始统计(这个k就是问题描述中的k)。为了记录到目前为止我们统计了多少个1,我们需要一个计数器。假设计数器有m位二进制:xm, ..., x1(从高位到低位)。我们至少可以总结出计数器的四个特性:

  1. 计数器有一个初始值,为了简单起见,置为0
  2. 对于数组中的每个输入,如果为0,计数器保持不变;
  3. 对于数组中的每个输入,如果为1,计数器加1
  4. 为了保证计数器表示的值能大于等于k,所以2^m >= k,等式两边取对数:m >= logk

这是关键部分:当我们扫描数组时,计数器的每一位(x1xm)是如何变化的?我们可以使用位操作,为了保证第2个特征,回想一下,那些位操作与0进行运算时,不会改变自身呢?没错,你说对了:x = x | 0x = x ^ 0

我们现在有表达式:x = x | i或者x = x ^ ii为数组元素。哪一个表达式更好呢?我们现在还不知道,所以我们来实际统计一下。

开始,计数器的所有位都初始化为0,即xm = 0, ... , x1 = 0。我们选择的位运算,要保证当扫描到数组元素为0时,计数器的每一位都不变。计数器的值为0,直到扫描到数组第一个为1的元素。当我们扫描到第一个为1的元素,计数器的每一位为:xm = 0, ..., x2 = 0, x1 = 1。让我们继续扫描,直到找到第二个为1的元素,这时计数器的每一位为:xm = 0, ..., x2 = 1, x1 = 0。注意,x11变为了0。对于x1 = x1 | i,经过2次计数(也就是2次x1 = x1 | 1)后,x1的值仍然为1。所有很明显,我们应该使用x1 = x1 ^ i。关于x2, ..., xm应该怎么计算呢?我们要找出x2, ..., xm改变值的条件。以x2做为示例。如果我们再扫描到一个1,想要改变x2的值,x1的值必须为多少呢?答案是:x1必须为1,因为x10的话,把x10变为1就可以了,而不必改变x2的值(其实这就是进位的原理,比如十进制要进位,必须要后一位为9)。所以,x2要改变值的条件是,x1i都为1,对应的算术表达式为x2 = x2 ^ (x1 & i)。同理,xm要改变值的条件是,xm-1, ..., x1i都为1,对应的算术表达式为:xm = xm ^ (xm-1 & ... & x1 & i)。没错,这就是我们找到的位操作方法。

然而,我们应该注意到上面的位操作中,计数是从02 ^ m - 1,而不是k。如果k满足k < 2 ^ m - 1,当计数器等于k时,我们需要一种重置机制把计数器重置为0。为了达到这个目的,我们申请一个变量mask,分别对计数器的每一位(xm, ..., x1)mask进行按位操作,即xm = xm & mask, ..., x1 = x1 & mask。如果我们能保证,只有当计数器的值等于k,并且统计的都是数组元素为1情况下,mask会为0,我们就达到了目的。我们是如何做到这一点的呢?试着想想用k计数与其他计数的区别。没错,它是统计元素为1的数量!对于每一次计数,计数器的每一位都有唯一的值,可以视为计数器的状态。如果我们把k写成二进制的形式:km, ..., k1,我们可以按照如下的方法来构造mask

// 如果kj = 1,yj = xj;kj = 0,yj = ~xj。j的取值范围是1到m。
mask = ~(y1 & y2 & ... & ym)

让我们来做一些例子:

k = 3: k1 = 1, k2 = 1, mask = ~(x1 & x2);
k = 5: k1 = 1, k2 = 0, k3 = 1, mask = ~(x1 & ~x2 & x3);

综上所述,我们的算法如下(nums为输入的数组):

for (int i : nums) {
    xm ^= (xm-1 & ... & x1 & i);
    xm-1 ^= (xm-2 & ... & x1 & i);
    .....
    x1 ^= i;
    
    // 如果kj = 1,yj = xj;kj = 0,yj = ~xj。j的取值范围是1到m。
    mask = ~(y1 & y2 & ... & ym);

    xm &= mask;
    ......
    x1 &= mask;
}

3、数组元素为32位整数的一般情况

现在我们把数组元素从1位推广到32位。一种最直接的方法是为32位整数中的每一位创建一个计数器(共32个计数器)。你可能在其他的文章中看到过这种解决方案。因为我们使用位操作,也许我们能“共同”管理这32个计数器。说是“共同管理”,是因为m的最小值满足m >= logk时,我们可以用m32位的整数代替32m位的计数器。能替代的原因是,位操作只作用于每个位,不同位上的操作是相互独立的(这是很明显的事,是吧?)。这允许我们将32个计数器的对应位放到一个32位整数中。下面的示意图展示了我们是如何做到这一点的。
在这里插入图片描述
第一行是一个32位的整数,对于每一位,我们都有一个对应的m位计数器(由向上箭头下方的列所示)。由于32位中的每一位操作都是相互独立的,所以我们可以将所有计数器的第m位放到一个32位整数中(如橙色框所示)。这个32位整数(表示为xm)的所有位都将遵循同样的位操作。由于每个计数器都有m位,所以我们得到了m个32位整数,对应第2部分中定义的x1, ..., xm,只不过是用32位的整数替换了1位的数字。因此,在上面的算法中,我们把x1, ..., xm看做是32位的整数,而不是1位的数字即可。其他的内容,都是一样的,到这里我们就扩展到了32位整数的一般情况了。


4、返回值

最后一个问题是,我们应该返回什么值,或者说x1xm中那个值与我们要返回的值相等。为了得到正确答案,我们需要理解x1, ..., xmm个32位整数分别代表什么。以x1为例,x132位,我们把它标记为rr的取值为132)。当我们扫描完数组后,x1r位的值由数组中所有元素第r位共同决定(更具体的说,数组中所有元素第r位为1的总数为q,定义q' = q % kq'的二进制形式为:q'm, ..., q'1,然后根据定义,x1r位的值等于q'1)。现在你可以问自己这样一个问题:x1的第r位为1意味着什么?

答案是要找到这个1是由哪些数组元素贡献的。一个出现k次的元素会起贡献吗?不会,为什么?因为一个元素要有贡献,它至少必须同时满足两个条件:这个元素的第r位为1,这个元素出现的次数不是k的整数倍。第一个条件很简单。第二个条件来自于这样一个事实:当1的数量等于k时,计数器将会重置为0,这意味着x1中相对应的位会被置为0。对于一个出现k次的元素,同时满足这两个条件是不可能的,所有它不可能贡献。最后,只有出现p (p % k != 0)次的元素才会贡献。如果p > k,那么这个元素前k * [p / k]([p/k]表示p/k向下取整)次也不会有贡献。因此我们设置p' = p % k,并说元素有效地出现了p'次。

我们把p'用二进制形式表示:p'm, ..., p'1(注意p' < k, 所以m个比特位足够用来表示它)。这里我声明xj等于所求元素的条件是p'j = 1j的取值为1m),下面给出快速证明。

如果xj的第r位为1,我们可以肯定的说,所求元素的第r位也为1(否则,没有任何元素能把xj的第r为置为1)。我们只需要证明,xj的第r位为0,所求元素的第r位只能为0。假设xjr位为0,而所求元素第r位为1,我们来看看会发生什么。扫描结束后,所求元素的这个1将会被统计p'次。根据定义,xj的第r位等于p'j,等于1。这和假设xj的第r位为0冲突。因此,我们得出结论,只要p'j = 1xj的第r位就总是和所求元素的第r位一样。这对于xj所有位都是成立的(即:r = 132都成立),所以我们可以得出结论,p'j = 1时,xj就等于所求元素。

要返回什么值,现在已经很明显了,用二进制形式表示p' = p % k,当p'j = 1时,返回xj即可。总结起来,算法的时间复杂度为O(n * logk),空间复杂度为O(logk)


附注: 这里有一个把xj的每一位、p'j、和所求元素s的每一位联系起来的通用公式:(xj)_r = s_r & p'j(xj)_rs_r分别表示xj和所求元素s的第r位。从公式中,当p'j = 1时,很容易得到(xj)_r = s_r,也就是说p'j = 1时,xj = s,和我们上面说的一样。此外当p'j = 0时,不管所求元素的值为多少,(xj)_r = 0,所以当p'j = 0时,xj都为0。所以我们得到结论:如果p'j = 1xj = s;如果p'j = 0xj = 0。这意味着表达式(x1 | x2 | ... | xm)的结果也等于所求元素s,因为这个表达式本质上是所求元素与自己和一些0进行运算,最终也会等于所求元素。


5、示例

这里有一些简单的例子来说明算法是如何运行的(你可以很容易的举出其他例子):

  1. k = 2, p = 1
    k2,所以m = 1,我们只需要一个32位的整数(x1)做为计数器即可。并且2 ^ m = k,所以我们也可以不用mask变量。完整的Java示例如下:
public int singleNumber(int[] nums) {
   int x1 = 0;
     
    for (int i : nums) {
        x1 ^= i;
    }
     
    return x1;
}
  1. k = 3, p = 1
    k3,所以m = 2,我们需要两个32位整数(x2x1)做为计数器。并且2 ^ m > k,所以我们需要mask变量。把k写成二进制形式:k = '11'k1 = 1k2 = 1,所以mask = ~(x1 & x2)。完整的Java示例如下:
public int singleNumber(int[] nums) {
    int x1 = 0, x2 = 0, mask = 0;
     
    for (int i : nums) {
        x2 ^= x1 & i;
        x1 ^= i;
        mask = ~(x1 & x2);
        x2 &= mask;
        x1 &= mask;
    }

	// 因为p = 1,二进制形式为 p = '01',所以p1 = 1,所以应该返回x1。
	// 如果p = 2,二进制形式为 p = '10',所以p2 = 1,所以应该返回x2。
	// 或者直接返回(x1 | x2)。
    return x1; 
}
  1. k = 5, p = 3
    k5,所以m = 3,我们需要三个32位整数(x3x2x1)做为计数器。并且2 ^ m > k,所以我们需要mask变量。把k写成二进制形式:k = '101'k1 = 1k2 = 0k3 = 1,所以mask = ~(x1 & ~x2 & x3)。完整的Java示例如下:
public int singleNumber(int[] nums) {
    int x1 = 0, x2 = 0, x3  = 0, mask = 0;

    for (int i : nums) {
        x3 ^= x2 & x1 & i;
        x2 ^= x1 & i;
        x1 ^= i;
        mask = ~(x1 & ~x2 & x3);
        x3 &= mask;
        x2 &= mask;
        x1 &= mask;
    }
    
    // 因为p = 3,二进制形式为 p = '11',所以p1 = p2 = 1,所以返回x1,或者x2都可以。
	// 如果p = 4,二进制形式为 p = '100',只有p3 = 1,所以x1、x2、x3中,只能返回x3。
	// 或者直接返回(x1 | x2 | x3)。
    return x1; 
}

最后,感谢那些为这篇文字提供反馈使它变得更好的人们。希望这篇文章对你有帮助,祝大家编码快乐。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值