前言
此为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(从高位到低位)。我们至少可以总结出计数器的四个特性:
- 计数器有一个初始值,为了简单起见,置为
0; - 对于数组中的每个输入,如果为
0,计数器保持不变; - 对于数组中的每个输入,如果为
1,计数器加1; - 为了保证计数器表示的值能大于等于
k,所以2^m >= k,等式两边取对数:m >= logk。
这是关键部分:当我们扫描数组时,计数器的每一位(x1到xm)是如何变化的?我们可以使用位操作,为了保证第2个特征,回想一下,那些位操作与0进行运算时,不会改变自身呢?没错,你说对了:x = x | 0 和x = x ^ 0。
我们现在有表达式:x = x | i或者x = x ^ i,i为数组元素。哪一个表达式更好呢?我们现在还不知道,所以我们来实际统计一下。
开始,计数器的所有位都初始化为0,即xm = 0, ... , x1 = 0。我们选择的位运算,要保证当扫描到数组元素为0时,计数器的每一位都不变。计数器的值为0,直到扫描到数组第一个为1的元素。当我们扫描到第一个为1的元素,计数器的每一位为:xm = 0, ..., x2 = 0, x1 = 1。让我们继续扫描,直到找到第二个为1的元素,这时计数器的每一位为:xm = 0, ..., x2 = 1, x1 = 0。注意,x1由1变为了0。对于x1 = x1 | i,经过2次计数(也就是2次x1 = x1 | 1)后,x1的值仍然为1。所有很明显,我们应该使用x1 = x1 ^ i。关于x2, ..., xm应该怎么计算呢?我们要找出x2, ..., xm改变值的条件。以x2做为示例。如果我们再扫描到一个1,想要改变x2的值,x1的值必须为多少呢?答案是:x1必须为1,因为x1为0的话,把x1由0变为1就可以了,而不必改变x2的值(其实这就是进位的原理,比如十进制要进位,必须要后一位为9)。所以,x2要改变值的条件是,x1和i都为1,对应的算术表达式为x2 = x2 ^ (x1 & i)。同理,xm要改变值的条件是,xm-1, ..., x1和i都为1,对应的算术表达式为:xm = xm ^ (xm-1 & ... & x1 & i)。没错,这就是我们找到的位操作方法。
然而,我们应该注意到上面的位操作中,计数是从0到2 ^ 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时,我们可以用m个32位的整数代替32个m位的计数器。能替代的原因是,位操作只作用于每个位,不同位上的操作是相互独立的(这是很明显的事,是吧?)。这允许我们将32个计数器的对应位放到一个32位整数中。下面的示意图展示了我们是如何做到这一点的。

第一行是一个32位的整数,对于每一位,我们都有一个对应的m位计数器(由向上箭头下方的列所示)。由于32位中的每一位操作都是相互独立的,所以我们可以将所有计数器的第m位放到一个32位整数中(如橙色框所示)。这个32位整数(表示为xm)的所有位都将遵循同样的位操作。由于每个计数器都有m位,所以我们得到了m个32位整数,对应第2部分中定义的x1, ..., xm,只不过是用32位的整数替换了1位的数字。因此,在上面的算法中,我们把x1, ..., xm看做是32位的整数,而不是1位的数字即可。其他的内容,都是一样的,到这里我们就扩展到了32位整数的一般情况了。
4、返回值
最后一个问题是,我们应该返回什么值,或者说x1到xm中那个值与我们要返回的值相等。为了得到正确答案,我们需要理解x1, ..., xm这m个32位整数分别代表什么。以x1为例,x1有32位,我们把它标记为r(r的取值为1到32)。当我们扫描完数组后,x1第r位的值由数组中所有元素第r位共同决定(更具体的说,数组中所有元素第r位为1的总数为q,定义q' = q % k,q'的二进制形式为:q'm, ..., q'1,然后根据定义,x1第r位的值等于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 = 1(j的取值为1到m),下面给出快速证明。
如果xj的第r位为1,我们可以肯定的说,所求元素的第r位也为1(否则,没有任何元素能把xj的第r为置为1)。我们只需要证明,xj的第r位为0,所求元素的第r位只能为0。假设xj第r位为0,而所求元素第r位为1,我们来看看会发生什么。扫描结束后,所求元素的这个1将会被统计p'次。根据定义,xj的第r位等于p'j,等于1。这和假设xj的第r位为0冲突。因此,我们得出结论,只要p'j = 1,xj的第r位就总是和所求元素的第r位一样。这对于xj所有位都是成立的(即:r = 1到32都成立),所以我们可以得出结论,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)_r和s_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 = 1,xj = s;如果p'j = 0,xj = 0。这意味着表达式(x1 | x2 | ... | xm)的结果也等于所求元素s,因为这个表达式本质上是所求元素与自己和一些0进行或运算,最终也会等于所求元素。
5、示例
这里有一些简单的例子来说明算法是如何运行的(你可以很容易的举出其他例子):
k = 2, p = 1
k为2,所以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;
}
k = 3, p = 1
k为3,所以m = 2,我们需要两个32位整数(x2,x1)做为计数器。并且2 ^ m > k,所以我们需要mask变量。把k写成二进制形式:k = '11',k1 = 1,k2 = 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;
}
k = 5, p = 3
k为5,所以m = 3,我们需要三个32位整数(x3,x2,x1)做为计数器。并且2 ^ m > k,所以我们需要mask变量。把k写成二进制形式:k = '101',k1 = 1,k2 = 0,k3 = 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;
}
最后,感谢那些为这篇文字提供反馈使它变得更好的人们。希望这篇文章对你有帮助,祝大家编码快乐。
331

被折叠的 条评论
为什么被折叠?



